ekjotsingh commited on
Commit
98f3f06
·
verified ·
1 Parent(s): 7598305

Update backtest.py

Browse files
Files changed (1) hide show
  1. backtest.py +57 -133
backtest.py CHANGED
@@ -1,5 +1,5 @@
1
  import matplotlib
2
- matplotlib.use('Agg') # Force headless mode for Spaces
3
 
4
  import yfinance as yf
5
  import pandas as pd
@@ -7,164 +7,88 @@ import numpy as np
7
  import matplotlib.pyplot as plt
8
  import os
9
 
10
- # --- CONFIGURATION ---
11
- START_DATE = "2008-01-01" # Start from 2008 to ensure all commodities have data
12
  INITIAL_CAPITAL = 100000
13
 
14
- # --- THE ASSET BASKET ---
15
  ASSETS = {
16
- "NIFTY": "^NSEI", # Indian Equity
17
- "GOLD": "GC=F", # Gold Futures
18
- "SILVER": "SI=F", # Silver Futures
19
- "OIL": "CL=F", # Crude Oil Futures
20
- "BONDS": "^TNX" # Risk-Free Proxy (10Y Yield)
21
  }
22
 
23
- def fetch_data():
24
- print(f"📥 Fetching Multi-Asset Basket from {START_DATE}...")
25
  try:
26
- # 1. Download all assets
27
- tickers = list(ASSETS.values())
28
- data = yf.download(tickers, start=START_DATE, progress=False)['Close']
29
-
30
- # 2. Fix Column Names
31
  if isinstance(data.columns, pd.MultiIndex):
32
  data.columns = [col[0] for col in data.columns]
33
 
34
- # Map tickers back to readable names (e.g., ^NSEI -> NIFTY)
35
- # Invert the dictionary for mapping
36
- ticker_to_name = {v: k for k, v in ASSETS.items()}
37
- data = data.rename(columns=ticker_to_name)
38
-
39
- # 3. Clean Data
40
- # Forward fill to handle different market holidays (US vs India)
41
  data = data.ffill().bfill()
42
 
43
- # Resample to Monthly for robust signals (less noise than daily)
44
- # We take the last price of each month
45
  monthly_data = data.resample('M').last()
 
46
 
47
- return data, monthly_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- except Exception as e:
50
- print(f"❌ Data Fetch Error: {e}")
51
- return None, None
52
-
53
- def strategy_momentum_rotation(daily_data, monthly_data):
54
- """
55
- Logic:
56
- 1. Calculate 6-Month Returns (Momentum) for all assets.
57
- 2. Rank them.
58
- 3. Pick Top 2 Winners every month.
59
- 4. Trend Filter: Only buy if Price > 200 Day SMA (Daily check).
60
- """
61
- print("⚙️ Running Momentum Rotation Engine...")
62
-
63
- # --- STEP 1: MOMENTUM CALCULATION (Monthly) ---
64
- # Calculate 6-month rolling return
65
- momentum = monthly_data.pct_change(6)
66
-
67
- # We exclude 'BONDS' from the momentum race (it's just a fallback)
68
- tradeable_assets = ["NIFTY", "GOLD", "SILVER", "OIL"]
69
- mom_scores = momentum[tradeable_assets]
70
-
71
- # --- STEP 2: RANKING ---
72
- # Rank assets 1 to 4 (4 is best, 1 is worst)
73
- ranks = mom_scores.rank(axis=1, ascending=True)
74
-
75
- # Create Weights: Top 2 assets get 50% each (0.5), others 0
76
- # Logic: If Rank >= 3 (Top 2 out of 4), Weight = 0.5
77
- target_weights = (ranks >= 3).astype(float) * 0.5
78
-
79
- # --- STEP 3: APPLY TO DAILY DATA (Rebalance) ---
80
- # We align monthly weights to daily data
81
- # .shift(1) prevents look-ahead bias (we use last month's data to trade this month)
82
- daily_weights = target_weights.reindex(daily_data.index).ffill().shift(1)
83
-
84
- # --- STEP 4: TREND FILTER (Safety Check) ---
85
- # Calculate 200-day SMA on daily data
86
- sma_200 = daily_data[tradeable_assets].rolling(200).mean()
87
-
88
- # Trend Rule: If Price < SMA200, Force Weight to 0 (Move that portion to Cash/Bonds)
89
- trend_filter = (daily_data[tradeable_assets] > sma_200).astype(int)
90
-
91
- # Final Weights = Momentum Weight * Trend Filter
92
- final_weights = daily_weights * trend_filter
93
-
94
- # Whatever is not invested goes to BONDS/CASH
95
- # Ex: If we only hold 1 asset (0.5), remainder (0.5) goes to Bonds.
96
- invested_sum = final_weights.sum(axis=1)
97
- cash_weight = 1.0 - invested_sum
98
-
99
- return final_weights, cash_weight
100
-
101
- def backtest_engine():
102
- # 1. Get Data
103
- daily_df, monthly_df = fetch_data()
104
- if daily_df is None: return "Error: No Data"
105
-
106
- # 2. Run Strategy
107
- asset_weights, cash_weight = strategy_momentum_rotation(daily_df, monthly_df)
108
-
109
- # 3. Calculate Portfolio Returns
110
- # Asset Returns
111
- asset_rets = daily_df[asset_weights.columns].pct_change()
112
-
113
- # Weighted Returns (Asset W * Asset Ret)
114
- port_asset_ret = (asset_weights * asset_rets).sum(axis=1)
115
-
116
- # Cash/Bond Return (Assume flat 4% annual risk-free if Bonds drop, or use Bond ETF return)
117
- # Using small constant return for cash component to simplify
118
- risk_free_daily = 0.04 / 252
119
- port_cash_ret = cash_weight * risk_free_daily
120
-
121
- # Total Strategy Return
122
- strategy_ret = port_asset_ret + port_cash_ret
123
-
124
- # Benchmark Return (Nifty Buy & Hold)
125
- benchmark_ret = daily_df['NIFTY'].pct_change()
126
-
127
- # 4. Wealth Index
128
- daily_df['Strategy_Wealth'] = INITIAL_CAPITAL * (1 + strategy_ret).cumprod()
129
- daily_df['Benchmark_Wealth'] = INITIAL_CAPITAL * (1 + benchmark_ret).cumprod()
130
-
131
- # 5. Plotting
132
- output_file = "backtest_result.png"
133
- try:
134
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [2, 1]})
135
 
136
- # Plot 1: Wealth Curve
137
- ax1.plot(daily_df.index, daily_df['Strategy_Wealth'], label='Momentum Rotation (Equity+Comm)', color='blue', linewidth=2)
138
- ax1.plot(daily_df.index, daily_df['Benchmark_Wealth'], label='Nifty 50 Buy & Hold', color='gray', linestyle='--', alpha=0.6)
139
- ax1.set_title("Strategy vs Benchmark (2008-Present)")
140
- ax1.set_ylabel("Wealth (INR)")
141
  ax1.set_yscale('log')
142
  ax1.legend()
143
- ax1.grid(True, which="both", alpha=0.3)
144
 
145
- # Plot 2: Asset Allocation (Stacked)
146
- # We plot the weights to show when we switched to Gold/Oil
147
- ax2.stackplot(asset_weights.index,
148
- asset_weights['NIFTY'], asset_weights['GOLD'],
149
- asset_weights['SILVER'], asset_weights['OIL'],
150
- cash_weight,
151
- labels=['Nifty', 'Gold', 'Silver', 'Oil', 'Cash'],
152
- alpha=0.6)
153
- ax2.set_title("Dynamic Asset Allocation")
154
- ax2.set_ylabel("Weight")
155
  ax2.legend(loc='upper left', fontsize='small')
 
156
 
157
  plt.tight_layout()
158
-
159
- # Save
160
- if os.path.exists(output_file): os.remove(output_file)
161
- plt.savefig(output_file, bbox_inches='tight')
162
  plt.close()
163
 
164
  return output_file
165
-
166
  except Exception as e:
167
- print(f" Plotting Error: {e}")
168
  return None
169
 
170
  if __name__ == "__main__":
 
1
  import matplotlib
2
+ matplotlib.use('Agg') # Force headless mode
3
 
4
  import yfinance as yf
5
  import pandas as pd
 
7
  import matplotlib.pyplot as plt
8
  import os
9
 
10
+ START_DATE = "2008-01-01"
 
11
  INITIAL_CAPITAL = 100000
12
 
 
13
  ASSETS = {
14
+ "NIFTY": "^NSEI",
15
+ "GOLD": "GC=F",
16
+ "SILVER": "SI=F",
17
+ "OIL": "CL=F",
18
+ "BONDS": "^TNX"
19
  }
20
 
21
+ def backtest_engine():
22
+ print("⚙️ Fetching Multi-Asset Basket...")
23
  try:
24
+ data = yf.download(list(ASSETS.values()), start=START_DATE, progress=False)['Close']
25
+ if data.empty: return None
26
+
27
+ # Clean Headers
 
28
  if isinstance(data.columns, pd.MultiIndex):
29
  data.columns = [col[0] for col in data.columns]
30
 
31
+ mapping = {v: k for k, v in ASSETS.items()}
32
+ data = data.rename(columns=mapping)
 
 
 
 
 
33
  data = data.ffill().bfill()
34
 
35
+ # Strategy: Monthly Momentum Rotation
 
36
  monthly_data = data.resample('M').last()
37
+ momentum = monthly_data.pct_change(6) # 6-month momentum
38
 
39
+ # Rank assets
40
+ tradeable = ["NIFTY", "GOLD", "SILVER", "OIL"]
41
+ ranks = momentum[tradeable].rank(axis=1)
42
+
43
+ # Top 2 get 50% each
44
+ target_weights = (ranks >= 3).astype(float) * 0.5
45
+
46
+ # Shift to avoid look-ahead bias and align to daily
47
+ daily_weights = target_weights.reindex(data.index).ffill().shift(1)
48
+
49
+ # Trend Filter (Price > 200 SMA)
50
+ sma_200 = data[tradeable].rolling(200).mean()
51
+ trend_filter = (data[tradeable] > sma_200).astype(int)
52
+
53
+ # Final Allocation
54
+ final_weights = daily_weights * trend_filter
55
+ cash_weight = 1.0 - final_weights.sum(axis=1)
56
+
57
+ # Returns
58
+ port_ret = (final_weights * data[tradeable].pct_change()).sum(axis=1)
59
+ # Add small risk-free rate for cash
60
+ port_ret += cash_weight * (0.04/252)
61
+
62
+ # Wealth
63
+ data['Strategy'] = INITIAL_CAPITAL * (1 + port_ret).cumprod()
64
+ data['Nifty_BuyHold'] = INITIAL_CAPITAL * (1 + data['NIFTY'].pct_change()).cumprod()
65
+
66
+ # Plot
67
+ output_file = "backtest_result.png"
68
+ if os.path.exists(output_file): os.remove(output_file)
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [2, 1]})
71
 
72
+ ax1.plot(data.index, data['Strategy'], label='Momentum Rotation', color='blue')
73
+ ax1.plot(data.index, data['Nifty_BuyHold'], label='Nifty Only', color='grey', linestyle='--')
 
 
 
74
  ax1.set_yscale('log')
75
  ax1.legend()
76
+ ax1.set_title("Strategy vs Benchmark")
77
 
78
+ ax2.stackplot(data.index, final_weights['NIFTY'], final_weights['GOLD'],
79
+ final_weights['SILVER'], final_weights['OIL'], cash_weight,
80
+ labels=['Nifty', 'Gold', 'Silver', 'Oil', 'Cash'], alpha=0.6)
 
 
 
 
 
 
 
81
  ax2.legend(loc='upper left', fontsize='small')
82
+ ax2.set_title("Asset Allocation")
83
 
84
  plt.tight_layout()
85
+ plt.savefig(output_file)
 
 
 
86
  plt.close()
87
 
88
  return output_file
89
+
90
  except Exception as e:
91
+ print(f"Backtest Error: {e}")
92
  return None
93
 
94
  if __name__ == "__main__":