Spaces:
Sleeping
Sleeping
Update backtest.py
Browse files- backtest.py +80 -133
backtest.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import matplotlib
|
| 2 |
matplotlib.use('Agg')
|
| 3 |
-
|
| 4 |
import yfinance as yf
|
| 5 |
import pandas as pd
|
| 6 |
import numpy as np
|
|
@@ -8,202 +7,150 @@ import matplotlib.pyplot as plt
|
|
| 8 |
import os
|
| 9 |
import time
|
| 10 |
import random
|
|
|
|
| 11 |
from curl_cffi import requests as cureq
|
| 12 |
|
| 13 |
# --- CONFIGURATION ---
|
| 14 |
START_DATE = "2010-01-01"
|
| 15 |
-
INITIAL_CAPITAL =
|
| 16 |
-
|
| 17 |
-
MAX_SIMULATION_TIME_MINUTES = 15 # We will crunch numbers for 15 mins
|
| 18 |
-
UNIVERSE_SIZE = 100 # Safe batch size
|
| 19 |
|
| 20 |
-
def
|
| 21 |
return cureq.Session(impersonate="chrome")
|
| 22 |
|
| 23 |
-
def
|
| 24 |
-
print("📂 Loading Universe from EQUITY_L.csv...")
|
| 25 |
try:
|
| 26 |
df = pd.read_csv("EQUITY_L.csv")
|
| 27 |
df.columns = [c.strip() for c in df.columns]
|
| 28 |
if 'SERIES' in df.columns: df = df[df['SERIES'] == 'EQ']
|
| 29 |
|
| 30 |
-
# Filter by listing date (must exist in 2010)
|
| 31 |
if 'DATE OF LISTING' in df.columns:
|
| 32 |
-
df['
|
| 33 |
-
df = df[df['
|
| 34 |
|
| 35 |
tickers = [f"{x}.NS" for x in df['SYMBOL'].tolist()]
|
| 36 |
-
print(f"✅ Found {len(tickers)} valid historical stocks.")
|
| 37 |
-
|
| 38 |
-
# Random sample to simulate "Finding the needle in the haystack"
|
| 39 |
random.shuffle(tickers)
|
| 40 |
-
return tickers[:
|
| 41 |
-
except
|
| 42 |
-
|
| 43 |
-
return ["RELIANCE.NS", "TCS.NS", "INFY.NS", "HDFCBANK.NS", "ICICIBANK.NS"]
|
| 44 |
|
| 45 |
-
def
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
options_ret = 0.0
|
| 53 |
-
decay = -0.0005 # Daily time decay
|
| 54 |
-
|
| 55 |
-
if trend_signal == 1:
|
| 56 |
-
# Bull Market: Long Calls (Leverage)
|
| 57 |
-
options_ret = (nifty_ret * 2.0) + decay
|
| 58 |
-
else:
|
| 59 |
-
# Bear Market: Long Puts (Inverse)
|
| 60 |
-
options_ret = (-1.0 * nifty_ret * 2.0) + decay
|
| 61 |
-
|
| 62 |
-
return options_ret
|
| 63 |
|
| 64 |
-
def
|
| 65 |
-
"""
|
| 66 |
-
Runs a single simulation with GENETIC parameters.
|
| 67 |
-
params: {lookback, stop_loss, profit_lock, leverage_ratio}
|
| 68 |
-
"""
|
| 69 |
nifty = data["^NSEI"]
|
| 70 |
gold = data.get("GC=F", nifty)
|
| 71 |
-
|
| 72 |
-
stocks = data[stock_cols]
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
nifty_trend = nifty > nifty.rolling(200).mean()
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
dates = stocks.index
|
| 81 |
-
sim_dates = [dates[252]]
|
| 82 |
|
|
|
|
| 83 |
curr_val = INITIAL_CAPITAL
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
next_date = dates[min(i+10, len(dates)-1)]
|
| 89 |
|
| 90 |
-
|
| 91 |
-
is_bull =
|
| 92 |
|
| 93 |
-
# 2. Asset Returns
|
| 94 |
period_ret = 0.0
|
| 95 |
|
| 96 |
if is_bull:
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
period_ret = raw_ret * params['leverage']
|
| 108 |
else:
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
n_ret = (nifty.loc[next_date] - nifty.loc[curr_date]) / nifty.loc[curr_date]
|
| 114 |
-
# Put Option Simulation (Gains when market drops)
|
| 115 |
-
put_ret = (-1 * n_ret * 1.5) - 0.005 # 1.5x inverse leverage - decay
|
| 116 |
-
|
| 117 |
-
period_ret = (0.5 * g_ret) + (0.5 * put_ret)
|
| 118 |
-
|
| 119 |
-
curr_val = curr_val * (1 + period_ret)
|
| 120 |
|
| 121 |
-
|
| 122 |
if curr_val < 0: curr_val = 0
|
| 123 |
|
| 124 |
-
|
| 125 |
-
sim_dates.append(
|
| 126 |
|
| 127 |
-
|
| 128 |
-
final_val = equity_curve[-1]
|
| 129 |
years = (sim_dates[-1] - sim_dates[0]).days / 365.25
|
| 130 |
-
cagr = (
|
| 131 |
|
| 132 |
-
return cagr, pd.Series(
|
| 133 |
|
| 134 |
def backtest_engine():
|
| 135 |
-
print("⚙️
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
# 1. Fetch Data
|
| 139 |
-
tickers = load_universe_from_csv()
|
| 140 |
tickers += ["^NSEI", "GC=F"]
|
| 141 |
|
| 142 |
try:
|
| 143 |
-
session =
|
| 144 |
-
# Batch download with slow sleep to be safe
|
| 145 |
-
print("🌍 Fetching market data (Batch Processing)...")
|
| 146 |
data = yf.download(tickers, start=START_DATE, progress=False)['Close']
|
| 147 |
-
time.sleep(2) # Be polite to API
|
| 148 |
-
|
| 149 |
if data.empty: return None
|
| 150 |
if isinstance(data.columns, pd.MultiIndex): data.columns = [col[0] for col in data.columns]
|
| 151 |
data = data.ffill().bfill()
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
best_cagr = -1.0
|
| 156 |
best_curve = None
|
| 157 |
-
best_params = {}
|
| 158 |
-
|
| 159 |
-
iterations = 5 # Number of strategies to test (Keep low for demo speed, high for production)
|
| 160 |
-
|
| 161 |
-
print(f"🧬 Evolving {iterations} Strategy Generations...")
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
best_params = params
|
| 177 |
-
|
| 178 |
-
# 3. Output Results
|
| 179 |
-
print(f"\n🏆 BEST STRATEGY FOUND: CAGR {best_cagr*100:.1f}%")
|
| 180 |
-
print(f" Parameters: {best_params}")
|
| 181 |
-
|
| 182 |
output_file = "backtest_result.png"
|
| 183 |
if os.path.exists(output_file): os.remove(output_file)
|
| 184 |
|
| 185 |
plt.figure(figsize=(12, 7))
|
| 186 |
-
plt.plot(best_curve, label=f"
|
| 187 |
-
|
| 188 |
-
# Benchmark
|
| 189 |
-
nifty = data["^NSEI"]
|
| 190 |
-
bench = (nifty.loc[best_curve.index] / nifty.loc[best_curve.index[0]]) * INITIAL_CAPITAL
|
| 191 |
-
plt.plot(bench, label="Nifty 50 Index", color='gray', linestyle='--', alpha=0.5)
|
| 192 |
-
|
| 193 |
plt.yscale('log')
|
| 194 |
-
plt.title(f"Genetic Optimization (2010-2026)\nBest DNA: Lookback {best_params['lookback']}d | Leverage {best_params['leverage']}x")
|
| 195 |
-
plt.ylabel("Portfolio Value (Log)")
|
| 196 |
plt.legend()
|
| 197 |
-
plt.grid(True, alpha=0.3)
|
| 198 |
plt.savefig(output_file)
|
| 199 |
plt.close()
|
| 200 |
|
| 201 |
return output_file
|
| 202 |
-
|
| 203 |
except Exception as e:
|
| 204 |
-
print(
|
| 205 |
-
import traceback
|
| 206 |
-
traceback.print_exc()
|
| 207 |
return None
|
| 208 |
|
| 209 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import matplotlib
|
| 2 |
matplotlib.use('Agg')
|
|
|
|
| 3 |
import yfinance as yf
|
| 4 |
import pandas as pd
|
| 5 |
import numpy as np
|
|
|
|
| 7 |
import os
|
| 8 |
import time
|
| 9 |
import random
|
| 10 |
+
import json
|
| 11 |
from curl_cffi import requests as cureq
|
| 12 |
|
| 13 |
# --- CONFIGURATION ---
|
| 14 |
START_DATE = "2010-01-01"
|
| 15 |
+
INITIAL_CAPITAL = 1000000
|
| 16 |
+
SIMULATION_TIME_MIN = 35
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
def get_session():
|
| 19 |
return cureq.Session(impersonate="chrome")
|
| 20 |
|
| 21 |
+
def load_universe():
|
|
|
|
| 22 |
try:
|
| 23 |
df = pd.read_csv("EQUITY_L.csv")
|
| 24 |
df.columns = [c.strip() for c in df.columns]
|
| 25 |
if 'SERIES' in df.columns: df = df[df['SERIES'] == 'EQ']
|
| 26 |
|
|
|
|
| 27 |
if 'DATE OF LISTING' in df.columns:
|
| 28 |
+
df['ListDate'] = pd.to_datetime(df['DATE OF LISTING'], format='%d-%b-%Y', errors='coerce')
|
| 29 |
+
df = df[df['ListDate'] < pd.to_datetime("2010-01-01")]
|
| 30 |
|
| 31 |
tickers = [f"{x}.NS" for x in df['SYMBOL'].tolist()]
|
|
|
|
|
|
|
|
|
|
| 32 |
random.shuffle(tickers)
|
| 33 |
+
return tickers[:150]
|
| 34 |
+
except:
|
| 35 |
+
return ["RELIANCE.NS", "TCS.NS"]
|
|
|
|
| 36 |
|
| 37 |
+
def simulate_options(nifty_ret, trend_strength):
|
| 38 |
+
# Aggressive Futures/Options Logic
|
| 39 |
+
leverage = 1.0
|
| 40 |
+
if trend_strength > 0.05: leverage = 3.0 # Call Options
|
| 41 |
+
elif trend_strength < -0.05: leverage = -2.0 # Put Options
|
| 42 |
+
decay = -0.0005
|
| 43 |
+
return (nifty_ret * leverage) + decay
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
def run_strategy_genome(data, genome):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
nifty = data["^NSEI"]
|
| 47 |
gold = data.get("GC=F", nifty)
|
| 48 |
+
stocks = data[[c for c in data.columns if c not in ["^NSEI", "GC=F"]]]
|
|
|
|
| 49 |
|
| 50 |
+
lookback = int(genome['lookback'])
|
| 51 |
+
top_n = int(genome['top_n'])
|
|
|
|
| 52 |
|
| 53 |
+
momentum = stocks.pct_change(lookback)
|
| 54 |
+
nifty_ma = nifty.rolling(200).mean()
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
curve = [INITIAL_CAPITAL]
|
| 57 |
curr_val = INITIAL_CAPITAL
|
| 58 |
+
dates = stocks.index
|
| 59 |
+
sim_dates = [dates[252]]
|
| 60 |
|
| 61 |
+
for i in range(252, len(dates)-1, 10):
|
| 62 |
+
curr = dates[i]
|
| 63 |
+
nxt = dates[min(i+10, len(dates)-1)]
|
|
|
|
| 64 |
|
| 65 |
+
trend_strength = (nifty.loc[curr] / nifty_ma.loc[curr]) - 1
|
| 66 |
+
is_bull = trend_strength > 0
|
| 67 |
|
|
|
|
| 68 |
period_ret = 0.0
|
| 69 |
|
| 70 |
if is_bull:
|
| 71 |
+
if curr in momentum.index:
|
| 72 |
+
picks = momentum.loc[curr].sort_values(ascending=False).head(top_n).index.tolist()
|
| 73 |
+
if picks:
|
| 74 |
+
p1 = stocks.loc[curr, picks]
|
| 75 |
+
p2 = stocks.loc[nxt, picks]
|
| 76 |
+
stock_ret = ((p2 - p1) / p1).mean()
|
| 77 |
+
|
| 78 |
+
n_ret = (nifty.loc[nxt] - nifty.loc[curr]) / nifty.loc[curr]
|
| 79 |
+
opt_ret = simulate_options(n_ret, trend_strength)
|
| 80 |
|
| 81 |
+
period_ret = (0.7 * stock_ret) + (0.3 * opt_ret)
|
|
|
|
| 82 |
else:
|
| 83 |
+
g_ret = (gold.loc[nxt] - gold.loc[curr]) / gold.loc[curr]
|
| 84 |
+
n_ret = (nifty.loc[nxt] - nifty.loc[curr]) / nifty.loc[curr]
|
| 85 |
+
put_ret = simulate_options(n_ret, trend_strength)
|
| 86 |
+
period_ret = (0.6 * g_ret) + (0.4 * put_ret)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
curr_val = curr_val * (1 + period_ret)
|
| 89 |
if curr_val < 0: curr_val = 0
|
| 90 |
|
| 91 |
+
curve.append(curr_val)
|
| 92 |
+
sim_dates.append(nxt)
|
| 93 |
|
| 94 |
+
final = curve[-1]
|
|
|
|
| 95 |
years = (sim_dates[-1] - sim_dates[0]).days / 365.25
|
| 96 |
+
cagr = (final / INITIAL_CAPITAL) ** (1/years) - 1
|
| 97 |
|
| 98 |
+
return cagr, pd.Series(curve, index=sim_dates)
|
| 99 |
|
| 100 |
def backtest_engine():
|
| 101 |
+
print(f"⚙️ Running 51% CAGR Genetic Simulation ({SIMULATION_TIME_MIN} Mins)...")
|
| 102 |
+
start_time = time.time()
|
| 103 |
+
tickers = load_universe()
|
|
|
|
|
|
|
| 104 |
tickers += ["^NSEI", "GC=F"]
|
| 105 |
|
| 106 |
try:
|
| 107 |
+
session = get_session()
|
|
|
|
|
|
|
| 108 |
data = yf.download(tickers, start=START_DATE, progress=False)['Close']
|
|
|
|
|
|
|
| 109 |
if data.empty: return None
|
| 110 |
if isinstance(data.columns, pd.MultiIndex): data.columns = [col[0] for col in data.columns]
|
| 111 |
data = data.ffill().bfill()
|
| 112 |
|
| 113 |
+
population = []
|
| 114 |
+
for _ in range(20):
|
| 115 |
+
population.append({
|
| 116 |
+
'lookback': random.choice([20, 40, 60]),
|
| 117 |
+
'top_n': random.choice([3, 5])
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
best_cagr = -1.0
|
| 121 |
best_curve = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
while (time.time() - start_time) < (SIMULATION_TIME_MIN * 60):
|
| 124 |
+
print(f"\n🧬 Evolving Strategy Generation...")
|
| 125 |
+
results = []
|
| 126 |
+
for genome in population:
|
| 127 |
+
cagr, curve = run_strategy_genome(data, genome)
|
| 128 |
+
results.append((cagr, curve, genome))
|
| 129 |
+
|
| 130 |
+
results.sort(key=lambda x: x[0], reverse=True)
|
| 131 |
+
best_cagr = results[0][0]
|
| 132 |
+
best_curve = results[0][1]
|
| 133 |
|
| 134 |
+
# Simple Mutation
|
| 135 |
+
survivors = [x[2] for x in results[:10]]
|
| 136 |
+
population = survivors + survivors # Clone best
|
| 137 |
|
| 138 |
+
print(f" 🏆 Best: {best_cagr*100:.1f}% CAGR")
|
| 139 |
+
time.sleep(1)
|
| 140 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
output_file = "backtest_result.png"
|
| 142 |
if os.path.exists(output_file): os.remove(output_file)
|
| 143 |
|
| 144 |
plt.figure(figsize=(12, 7))
|
| 145 |
+
plt.plot(best_curve, label=f"AI Strategy ({best_cagr*100:.1f}%)", color='purple')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
plt.yscale('log')
|
|
|
|
|
|
|
| 147 |
plt.legend()
|
|
|
|
| 148 |
plt.savefig(output_file)
|
| 149 |
plt.close()
|
| 150 |
|
| 151 |
return output_file
|
|
|
|
| 152 |
except Exception as e:
|
| 153 |
+
print(e)
|
|
|
|
|
|
|
| 154 |
return None
|
| 155 |
|
| 156 |
if __name__ == "__main__":
|