Skip to content

Commit 5e1d988

Browse files
add backtesting
1 parent bd423eb commit 5e1d988

File tree

9 files changed

+799
-52
lines changed

9 files changed

+799
-52
lines changed

Chapter5/time_series.ipynb

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3322,6 +3322,290 @@
33223322
"source": [
33233323
"[Link to tsmoothie](https://bit.ly/3L8KXky)."
33243324
]
3325+
},
3326+
{
3327+
"cell_type": "markdown",
3328+
"id": "f2e850ef",
3329+
"metadata": {},
3330+
"source": [
3331+
"### Backtesting: Assess Trading Strategy Performance Effortlessly in Python"
3332+
]
3333+
},
3334+
{
3335+
"cell_type": "code",
3336+
"execution_count": null,
3337+
"id": "0ff27227",
3338+
"metadata": {
3339+
"tags": [
3340+
"hide-cell"
3341+
]
3342+
},
3343+
"outputs": [],
3344+
"source": [
3345+
"!pip install -U backtesting"
3346+
]
3347+
},
3348+
{
3349+
"cell_type": "markdown",
3350+
"id": "0288e5d6",
3351+
"metadata": {},
3352+
"source": [
3353+
"Evaluating trading strategies' effectiveness is crucial for financial decision-making, but it's challenging due to the complexities of historical data analysis and strategy testing. \n",
3354+
"\n",
3355+
"Backtesting allows users to simulate trades based on historical data and visualize the outcomes through interactive plots in three lines of code.\n",
3356+
"\n",
3357+
"To see how Backtesting works, let's create our first strategy to backtest on these Google data, a simple moving average (MA) cross-over strategy."
3358+
]
3359+
},
3360+
{
3361+
"cell_type": "code",
3362+
"execution_count": 1,
3363+
"id": "4f410242",
3364+
"metadata": {},
3365+
"outputs": [
3366+
{
3367+
"data": {
3368+
"text/html": [
3369+
"<div>\n",
3370+
"<style scoped>\n",
3371+
" .dataframe tbody tr th:only-of-type {\n",
3372+
" vertical-align: middle;\n",
3373+
" }\n",
3374+
"\n",
3375+
" .dataframe tbody tr th {\n",
3376+
" vertical-align: top;\n",
3377+
" }\n",
3378+
"\n",
3379+
" .dataframe thead th {\n",
3380+
" text-align: right;\n",
3381+
" }\n",
3382+
"</style>\n",
3383+
"<table border=\"1\" class=\"dataframe\">\n",
3384+
" <thead>\n",
3385+
" <tr style=\"text-align: right;\">\n",
3386+
" <th></th>\n",
3387+
" <th>Open</th>\n",
3388+
" <th>High</th>\n",
3389+
" <th>Low</th>\n",
3390+
" <th>Close</th>\n",
3391+
" <th>Volume</th>\n",
3392+
" </tr>\n",
3393+
" </thead>\n",
3394+
" <tbody>\n",
3395+
" <tr>\n",
3396+
" <th>2013-02-25</th>\n",
3397+
" <td>802.3</td>\n",
3398+
" <td>808.41</td>\n",
3399+
" <td>790.49</td>\n",
3400+
" <td>790.77</td>\n",
3401+
" <td>2303900</td>\n",
3402+
" </tr>\n",
3403+
" <tr>\n",
3404+
" <th>2013-02-26</th>\n",
3405+
" <td>795.0</td>\n",
3406+
" <td>795.95</td>\n",
3407+
" <td>784.40</td>\n",
3408+
" <td>790.13</td>\n",
3409+
" <td>2202500</td>\n",
3410+
" </tr>\n",
3411+
" <tr>\n",
3412+
" <th>2013-02-27</th>\n",
3413+
" <td>794.8</td>\n",
3414+
" <td>804.75</td>\n",
3415+
" <td>791.11</td>\n",
3416+
" <td>799.78</td>\n",
3417+
" <td>2026100</td>\n",
3418+
" </tr>\n",
3419+
" <tr>\n",
3420+
" <th>2013-02-28</th>\n",
3421+
" <td>801.1</td>\n",
3422+
" <td>806.99</td>\n",
3423+
" <td>801.03</td>\n",
3424+
" <td>801.20</td>\n",
3425+
" <td>2265800</td>\n",
3426+
" </tr>\n",
3427+
" <tr>\n",
3428+
" <th>2013-03-01</th>\n",
3429+
" <td>797.8</td>\n",
3430+
" <td>807.14</td>\n",
3431+
" <td>796.15</td>\n",
3432+
" <td>806.19</td>\n",
3433+
" <td>2175400</td>\n",
3434+
" </tr>\n",
3435+
" </tbody>\n",
3436+
"</table>\n",
3437+
"</div>"
3438+
],
3439+
"text/plain": [
3440+
" Open High Low Close Volume\n",
3441+
"2013-02-25 802.3 808.41 790.49 790.77 2303900\n",
3442+
"2013-02-26 795.0 795.95 784.40 790.13 2202500\n",
3443+
"2013-02-27 794.8 804.75 791.11 799.78 2026100\n",
3444+
"2013-02-28 801.1 806.99 801.03 801.20 2265800\n",
3445+
"2013-03-01 797.8 807.14 796.15 806.19 2175400"
3446+
]
3447+
},
3448+
"execution_count": 1,
3449+
"metadata": {},
3450+
"output_type": "execute_result"
3451+
}
3452+
],
3453+
"source": [
3454+
"from backtesting.test import GOOG\n",
3455+
"\n",
3456+
"GOOG.tail()"
3457+
]
3458+
},
3459+
{
3460+
"cell_type": "code",
3461+
"execution_count": 2,
3462+
"id": "b3230aaf",
3463+
"metadata": {},
3464+
"outputs": [],
3465+
"source": [
3466+
"import pandas as pd\n",
3467+
"\n",
3468+
"\n",
3469+
"def SMA(values, n):\n",
3470+
" \"\"\"\n",
3471+
" Return simple moving average of `values`, at\n",
3472+
" each step taking into account `n` previous values.\n",
3473+
" \"\"\"\n",
3474+
" return pd.Series(values).rolling(n).mean()"
3475+
]
3476+
},
3477+
{
3478+
"cell_type": "code",
3479+
"execution_count": 3,
3480+
"id": "cbb93184",
3481+
"metadata": {},
3482+
"outputs": [],
3483+
"source": [
3484+
"from backtesting import Strategy\n",
3485+
"from backtesting.lib import crossover\n",
3486+
"\n",
3487+
"\n",
3488+
"class SmaCross(Strategy):\n",
3489+
" # Define the two MA lags as *class variables*\n",
3490+
" # for later optimization\n",
3491+
" n1 = 10\n",
3492+
" n2 = 20\n",
3493+
"\n",
3494+
" def init(self):\n",
3495+
" # Precompute the two moving averages\n",
3496+
" self.sma1 = self.I(SMA, self.data.Close, self.n1)\n",
3497+
" self.sma2 = self.I(SMA, self.data.Close, self.n2)\n",
3498+
"\n",
3499+
" def next(self):\n",
3500+
" # If sma1 crosses above sma2, close any existing\n",
3501+
" # short trades, and buy the asset\n",
3502+
" if crossover(self.sma1, self.sma2):\n",
3503+
" self.position.close()\n",
3504+
" self.buy()\n",
3505+
"\n",
3506+
" # Else, if sma1 crosses below sma2, close any existing\n",
3507+
" # long trades, and sell the asset\n",
3508+
" elif crossover(self.sma2, self.sma1):\n",
3509+
" self.position.close()\n",
3510+
" self.sell()"
3511+
]
3512+
},
3513+
{
3514+
"cell_type": "markdown",
3515+
"id": "94bf31a7",
3516+
"metadata": {},
3517+
"source": [
3518+
"To assess the performance of our investment strategy, we will instantiate a `Backtest` object, using Google stock data as our asset of interest and incorporating the `SmaCross` strategy class. We'll start with an initial cash balance of 10,000 units and set the broker's commission to a realistic rate of 0.2%."
3519+
]
3520+
},
3521+
{
3522+
"cell_type": "code",
3523+
"execution_count": 4,
3524+
"id": "4c73c152",
3525+
"metadata": {},
3526+
"outputs": [
3527+
{
3528+
"data": {
3529+
"text/plain": [
3530+
"Start 2004-08-19 00:00:00\n",
3531+
"End 2013-03-01 00:00:00\n",
3532+
"Duration 3116 days 00:00:00\n",
3533+
"Exposure Time [%] 97.067039\n",
3534+
"Equity Final [$] 68221.96986\n",
3535+
"Equity Peak [$] 68991.21986\n",
3536+
"Return [%] 582.219699\n",
3537+
"Buy & Hold Return [%] 703.458242\n",
3538+
"Return (Ann.) [%] 25.266427\n",
3539+
"Volatility (Ann.) [%] 38.383008\n",
3540+
"Sharpe Ratio 0.658271\n",
3541+
"Sortino Ratio 1.288779\n",
3542+
"Calmar Ratio 0.763748\n",
3543+
"Max. Drawdown [%] -33.082172\n",
3544+
"Avg. Drawdown [%] -5.581506\n",
3545+
"Max. Drawdown Duration 688 days 00:00:00\n",
3546+
"Avg. Drawdown Duration 41 days 00:00:00\n",
3547+
"# Trades 94\n",
3548+
"Win Rate [%] 54.255319\n",
3549+
"Best Trade [%] 57.11931\n",
3550+
"Worst Trade [%] -16.629898\n",
3551+
"Avg. Trade [%] 2.074326\n",
3552+
"Max. Trade Duration 121 days 00:00:00\n",
3553+
"Avg. Trade Duration 33 days 00:00:00\n",
3554+
"Profit Factor 2.190805\n",
3555+
"Expectancy [%] 2.606294\n",
3556+
"SQN 1.990216\n",
3557+
"_strategy SmaCross\n",
3558+
"_equity_curve ...\n",
3559+
"_trades Size EntryB...\n",
3560+
"dtype: object"
3561+
]
3562+
},
3563+
"execution_count": 4,
3564+
"metadata": {},
3565+
"output_type": "execute_result"
3566+
}
3567+
],
3568+
"source": [
3569+
"from backtesting import Backtest\n",
3570+
"\n",
3571+
"bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002)\n",
3572+
"stats = bt.run()\n",
3573+
"stats"
3574+
]
3575+
},
3576+
{
3577+
"cell_type": "markdown",
3578+
"id": "f3c91c05",
3579+
"metadata": {},
3580+
"source": [
3581+
"Plot the outcomes:"
3582+
]
3583+
},
3584+
{
3585+
"cell_type": "code",
3586+
"execution_count": null,
3587+
"id": "7a5f7bf7",
3588+
"metadata": {},
3589+
"outputs": [],
3590+
"source": [
3591+
"bt.plot()"
3592+
]
3593+
},
3594+
{
3595+
"cell_type": "markdown",
3596+
"id": "00f1fa47",
3597+
"metadata": {},
3598+
"source": [
3599+
"![](../img/backtesting.png)"
3600+
]
3601+
},
3602+
{
3603+
"cell_type": "markdown",
3604+
"id": "f9e0ead1",
3605+
"metadata": {},
3606+
"source": [
3607+
"[Link to Backtesting](https://bit.ly/3zwi0g3)."
3608+
]
33253609
}
33263610
],
33273611
"metadata": {

Chapter5/visualization.ipynb

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4541,24 +4541,14 @@
45414541
},
45424542
{
45434543
"cell_type": "code",
4544-
"execution_count": 1,
4544+
"execution_count": null,
45454545
"id": "13c7d4b2",
45464546
"metadata": {
45474547
"tags": [
45484548
"hide-cell"
45494549
]
45504550
},
4551-
"outputs": [
4552-
{
4553-
"name": "stdout",
4554-
"output_type": "stream",
4555-
"text": [
4556-
"\n",
4557-
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.1.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.1.1\u001b[0m\n",
4558-
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"
4559-
]
4560-
}
4561-
],
4551+
"outputs": [],
45624552
"source": [
45634553
"!pip install pygwalker -q"
45644554
]
@@ -4573,6 +4563,14 @@
45734563
"PyGWalker simplifies the process of creating visualizations by allowing users to drag and drop variables to create charts without writing much code."
45744564
]
45754565
},
4566+
{
4567+
"cell_type": "markdown",
4568+
"id": "7d24c9e6",
4569+
"metadata": {},
4570+
"source": [
4571+
"You can use PyGWalker without changing your existing workflow. For example, you can call up PyGWalker with the Dataframe loaded in this way:\n"
4572+
]
4573+
},
45764574
{
45774575
"cell_type": "code",
45784576
"execution_count": 2,
@@ -4584,17 +4582,9 @@
45844582
"import pandas as pd"
45854583
]
45864584
},
4587-
{
4588-
"cell_type": "markdown",
4589-
"id": "7d24c9e6",
4590-
"metadata": {},
4591-
"source": [
4592-
"You can use pygwalker without changing your existing workflow. For example, you can call up Graphic Walker with the dataframe loaded in this way:\n"
4593-
]
4594-
},
45954585
{
45964586
"cell_type": "code",
4597-
"execution_count": 4,
4587+
"execution_count": 8,
45984588
"id": "21ea9ef0",
45994589
"metadata": {},
46004590
"outputs": [
@@ -4870,7 +4860,7 @@
48704860
"9 6 "
48714861
]
48724862
},
4873-
"execution_count": 4,
4863+
"execution_count": 8,
48744864
"metadata": {},
48754865
"output_type": "execute_result"
48764866
}

0 commit comments

Comments
 (0)