Spaces:
Sleeping
Sleeping
Update backtest.py
Browse files- backtest.py +34 -41
backtest.py
CHANGED
|
@@ -24,18 +24,12 @@ def load_universe():
|
|
| 24 |
df = df[df['ListDate'] < pd.to_datetime("2010-01-01")]
|
| 25 |
|
| 26 |
tickers = [f"{x}.NS" for x in df['SYMBOL'].tolist()]
|
| 27 |
-
print(f"✅ Loaded {len(tickers)} historical tickers for
|
| 28 |
random.shuffle(tickers)
|
| 29 |
return tickers[:250]
|
| 30 |
except:
|
| 31 |
return ["RELIANCE.NS", "TCS.NS", "INFY.NS", "SBIN.NS", "HDFCBANK.NS", "ITC.NS"]
|
| 32 |
|
| 33 |
-
def simulate_options(nifty_ret, trend_strength):
|
| 34 |
-
leverage = 1.0
|
| 35 |
-
if trend_strength > 0.05: leverage = 2.0
|
| 36 |
-
elif trend_strength < -0.05: leverage = -2.0
|
| 37 |
-
return (nifty_ret * leverage) - 0.0005
|
| 38 |
-
|
| 39 |
def run_strategy_genome(data, genome):
|
| 40 |
if data.empty: return -1.0, []
|
| 41 |
|
|
@@ -50,9 +44,10 @@ def run_strategy_genome(data, genome):
|
|
| 50 |
top_n = int(genome['top_n'])
|
| 51 |
rebalance_days = int(genome['rebalance'])
|
| 52 |
stop_loss = float(genome['stop_loss'])
|
|
|
|
| 53 |
|
| 54 |
momentum = stocks.pct_change(lookback)
|
| 55 |
-
nifty_ma = nifty.rolling(
|
| 56 |
|
| 57 |
curve = [INITIAL_CAPITAL]
|
| 58 |
curr_val = INITIAL_CAPITAL
|
|
@@ -66,33 +61,33 @@ def run_strategy_genome(data, genome):
|
|
| 66 |
nxt = dates[min(i+rebalance_days, len(dates)-1)]
|
| 67 |
|
| 68 |
try:
|
| 69 |
-
|
| 70 |
-
is_bull =
|
| 71 |
period_ret = 0.0
|
| 72 |
|
| 73 |
if is_bull:
|
|
|
|
| 74 |
if curr in momentum.index:
|
| 75 |
scores = momentum.loc[curr]
|
| 76 |
-
scores = scores[scores > 0]
|
| 77 |
picks = scores.sort_values(ascending=False).head(top_n).index.tolist()
|
| 78 |
|
| 79 |
if len(picks) > 0:
|
| 80 |
p1 = stocks.loc[curr, picks]
|
| 81 |
p2 = stocks.loc[nxt, picks]
|
|
|
|
|
|
|
| 82 |
stock_ret = ((p2 - p1) / p1).mean()
|
| 83 |
if pd.isna(stock_ret): stock_ret = 0.0
|
| 84 |
|
| 85 |
-
|
| 86 |
-
opt_ret = simulate_options(n_ret, trend_strength)
|
| 87 |
-
|
| 88 |
-
period_ret = (0.8 * stock_ret) + (0.2 * opt_ret)
|
| 89 |
else:
|
|
|
|
| 90 |
g_ret = (gold.loc[nxt] - gold.loc[curr]) / gold.loc[curr]
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
period_ret = (0.5 * g_ret) + (0.5 * put_ret)
|
| 94 |
|
| 95 |
-
|
| 96 |
if period_ret < -stop_loss: period_ret = -stop_loss
|
| 97 |
|
| 98 |
curr_val = curr_val * (1 + period_ret)
|
|
@@ -112,7 +107,7 @@ def run_strategy_genome(data, genome):
|
|
| 112 |
return cagr, pd.Series(curve, index=sim_dates)
|
| 113 |
|
| 114 |
def backtest_engine():
|
| 115 |
-
print(f"⚙️ Initializing
|
| 116 |
start_time = time.time()
|
| 117 |
|
| 118 |
tickers = load_universe()
|
|
@@ -126,23 +121,24 @@ def backtest_engine():
|
|
| 126 |
data = data.ffill().bfill()
|
| 127 |
if data.empty: return None
|
| 128 |
|
|
|
|
| 129 |
population = []
|
| 130 |
for _ in range(30):
|
| 131 |
population.append({
|
| 132 |
-
'lookback': random.choice([10, 20, 30, 45, 60
|
| 133 |
-
'top_n': random.choice([
|
| 134 |
-
'rebalance': random.choice([3, 5, 7, 10
|
| 135 |
-
'stop_loss': random.choice([0.02, 0.04, 0.06,
|
|
|
|
| 136 |
})
|
| 137 |
|
| 138 |
best_cagr = -1.0
|
| 139 |
-
prev_cagr = -1.0
|
| 140 |
best_curve = None
|
| 141 |
stall_count = 0
|
| 142 |
generation = 1
|
| 143 |
|
| 144 |
while (time.time() - start_time) < (SIMULATION_TIME_MIN * 60):
|
| 145 |
-
print(f"\n🧬 Generation {generation}: Evaluating Portfolios...")
|
| 146 |
results = []
|
| 147 |
|
| 148 |
for genome in population:
|
|
@@ -154,7 +150,6 @@ def backtest_engine():
|
|
| 154 |
if results:
|
| 155 |
current_top_cagr = results[0][0]
|
| 156 |
|
| 157 |
-
# Stall Detection Logic
|
| 158 |
if current_top_cagr > best_cagr + 0.001:
|
| 159 |
best_cagr = current_top_cagr
|
| 160 |
best_curve = results[0][1]
|
|
@@ -164,17 +159,15 @@ def backtest_engine():
|
|
| 164 |
stall_count += 1
|
| 165 |
|
| 166 |
print(f" 🏆 Best: {best_cagr*100:.1f}% CAGR")
|
| 167 |
-
print(f" 🧬
|
| 168 |
|
| 169 |
if stall_count >= 3:
|
| 170 |
-
print(" ⚠️
|
| 171 |
|
| 172 |
-
survivors = [x[2] for x in results[:6]]
|
| 173 |
new_pop = list(survivors)
|
| 174 |
|
| 175 |
-
# --- CROSSOVER & MUTATION ENGINE ---
|
| 176 |
while len(new_pop) < 30:
|
| 177 |
-
# Mating: Combine traits from two top strategies
|
| 178 |
p1 = random.choice(survivors)
|
| 179 |
p2 = random.choice(survivors)
|
| 180 |
|
|
@@ -182,20 +175,20 @@ def backtest_engine():
|
|
| 182 |
'lookback': p1['lookback'] if random.random() > 0.5 else p2['lookback'],
|
| 183 |
'top_n': p1['top_n'] if random.random() > 0.5 else p2['top_n'],
|
| 184 |
'rebalance': p1['rebalance'] if random.random() > 0.5 else p2['rebalance'],
|
| 185 |
-
'stop_loss': p1['stop_loss'] if random.random() > 0.5 else p2['stop_loss']
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
-
# Hyper-Mutation triggers if stuck
|
| 189 |
mutation_rate = 0.8 if stall_count >= 3 else 0.3
|
| 190 |
|
| 191 |
-
if random.random() < mutation_rate: child['lookback'] = random.choice([10, 20, 30, 45, 60
|
| 192 |
-
if random.random() < mutation_rate: child['top_n'] = random.choice([
|
| 193 |
-
if random.random() < mutation_rate: child['rebalance'] = random.choice([3, 5, 7, 10
|
| 194 |
-
if random.random() < mutation_rate: child['stop_loss'] = random.choice([0.02, 0.04, 0.06
|
|
|
|
| 195 |
|
| 196 |
new_pop.append(child)
|
| 197 |
|
| 198 |
-
# If stuck for 4 rounds, wipe the board except for the #1 strategy
|
| 199 |
if stall_count >= 4:
|
| 200 |
stall_count = 0
|
| 201 |
|
|
@@ -209,14 +202,14 @@ def backtest_engine():
|
|
| 209 |
|
| 210 |
if best_curve is not None:
|
| 211 |
plt.figure(figsize=(12, 7))
|
| 212 |
-
plt.plot(best_curve, label=f"
|
| 213 |
|
| 214 |
nifty = data["^NSEI"]
|
| 215 |
bench = (nifty.loc[best_curve.index] / nifty.loc[best_curve.index[0]]) * INITIAL_CAPITAL
|
| 216 |
plt.plot(bench, label="Nifty 50 Index", color='gray', linestyle='--')
|
| 217 |
|
| 218 |
plt.yscale('log')
|
| 219 |
-
plt.title("Renaissance
|
| 220 |
plt.ylabel("Portfolio Value (Log Scale)")
|
| 221 |
plt.legend()
|
| 222 |
plt.grid(True, alpha=0.3)
|
|
|
|
| 24 |
df = df[df['ListDate'] < pd.to_datetime("2010-01-01")]
|
| 25 |
|
| 26 |
tickers = [f"{x}.NS" for x in df['SYMBOL'].tolist()]
|
| 27 |
+
print(f"✅ Loaded {len(tickers)} historical tickers for Pure Alpha Engine.")
|
| 28 |
random.shuffle(tickers)
|
| 29 |
return tickers[:250]
|
| 30 |
except:
|
| 31 |
return ["RELIANCE.NS", "TCS.NS", "INFY.NS", "SBIN.NS", "HDFCBANK.NS", "ITC.NS"]
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
def run_strategy_genome(data, genome):
|
| 34 |
if data.empty: return -1.0, []
|
| 35 |
|
|
|
|
| 44 |
top_n = int(genome['top_n'])
|
| 45 |
rebalance_days = int(genome['rebalance'])
|
| 46 |
stop_loss = float(genome['stop_loss'])
|
| 47 |
+
trend_filter_days = int(genome['trend_filter']) # AI chosen crash detector
|
| 48 |
|
| 49 |
momentum = stocks.pct_change(lookback)
|
| 50 |
+
nifty_ma = nifty.rolling(trend_filter_days).mean()
|
| 51 |
|
| 52 |
curve = [INITIAL_CAPITAL]
|
| 53 |
curr_val = INITIAL_CAPITAL
|
|
|
|
| 61 |
nxt = dates[min(i+rebalance_days, len(dates)-1)]
|
| 62 |
|
| 63 |
try:
|
| 64 |
+
# 1.0x Maximum Capital Exposure Rule Enforced Here
|
| 65 |
+
is_bull = nifty.loc[curr] > nifty_ma.loc[curr]
|
| 66 |
period_ret = 0.0
|
| 67 |
|
| 68 |
if is_bull:
|
| 69 |
+
# ZERO LEVERAGE: 100% Cash Equities
|
| 70 |
if curr in momentum.index:
|
| 71 |
scores = momentum.loc[curr]
|
| 72 |
+
scores = scores[scores > 0] # Must have positive absolute momentum
|
| 73 |
picks = scores.sort_values(ascending=False).head(top_n).index.tolist()
|
| 74 |
|
| 75 |
if len(picks) > 0:
|
| 76 |
p1 = stocks.loc[curr, picks]
|
| 77 |
p2 = stocks.loc[nxt, picks]
|
| 78 |
+
|
| 79 |
+
# 100% of money split evenly among the tight basket
|
| 80 |
stock_ret = ((p2 - p1) / p1).mean()
|
| 81 |
if pd.isna(stock_ret): stock_ret = 0.0
|
| 82 |
|
| 83 |
+
period_ret = stock_ret
|
|
|
|
|
|
|
|
|
|
| 84 |
else:
|
| 85 |
+
# ZERO LEVERAGE BEAR HEDGE: 100% Gold (Safe Haven Investing)
|
| 86 |
g_ret = (gold.loc[nxt] - gold.loc[curr]) / gold.loc[curr]
|
| 87 |
+
if pd.isna(g_ret): g_ret = 0.0
|
| 88 |
+
period_ret = g_ret
|
|
|
|
| 89 |
|
| 90 |
+
# Stop Loss Execution
|
| 91 |
if period_ret < -stop_loss: period_ret = -stop_loss
|
| 92 |
|
| 93 |
curr_val = curr_val * (1 + period_ret)
|
|
|
|
| 107 |
return cagr, pd.Series(curve, index=sim_dates)
|
| 108 |
|
| 109 |
def backtest_engine():
|
| 110 |
+
print(f"⚙️ Initializing Zero-Leverage 'Pure Alpha' Simulator...")
|
| 111 |
start_time = time.time()
|
| 112 |
|
| 113 |
tickers = load_universe()
|
|
|
|
| 121 |
data = data.ffill().bfill()
|
| 122 |
if data.empty: return None
|
| 123 |
|
| 124 |
+
# --- AI GENOME EXPANSION ---
|
| 125 |
population = []
|
| 126 |
for _ in range(30):
|
| 127 |
population.append({
|
| 128 |
+
'lookback': random.choice([5, 10, 15, 20, 30, 45, 60]), # Faster signal
|
| 129 |
+
'top_n': random.choice([2, 3, 4, 5]), # Tight Sector Basket
|
| 130 |
+
'rebalance': random.choice([2, 3, 5, 7, 10]), # High Velocity
|
| 131 |
+
'stop_loss': random.choice([0.02, 0.04, 0.06]), # Tight Risk
|
| 132 |
+
'trend_filter': random.choice([30, 50, 100, 200]) # AI decides crash sensitivity
|
| 133 |
})
|
| 134 |
|
| 135 |
best_cagr = -1.0
|
|
|
|
| 136 |
best_curve = None
|
| 137 |
stall_count = 0
|
| 138 |
generation = 1
|
| 139 |
|
| 140 |
while (time.time() - start_time) < (SIMULATION_TIME_MIN * 60):
|
| 141 |
+
print(f"\n🧬 Generation {generation}: Evaluating Portfolios (Max 1.0x Exposure)...")
|
| 142 |
results = []
|
| 143 |
|
| 144 |
for genome in population:
|
|
|
|
| 150 |
if results:
|
| 151 |
current_top_cagr = results[0][0]
|
| 152 |
|
|
|
|
| 153 |
if current_top_cagr > best_cagr + 0.001:
|
| 154 |
best_cagr = current_top_cagr
|
| 155 |
best_curve = results[0][1]
|
|
|
|
| 159 |
stall_count += 1
|
| 160 |
|
| 161 |
print(f" 🏆 Best: {best_cagr*100:.1f}% CAGR")
|
| 162 |
+
print(f" 🧬 DNA: Lookback {best_dna['lookback']}d | Holds Top {best_dna['top_n']} Stocks | Rebalance: {best_dna['rebalance']}d | Regime Filter: {best_dna['trend_filter']}d SMA")
|
| 163 |
|
| 164 |
if stall_count >= 3:
|
| 165 |
+
print(" ⚠️ Evolution Stalled. Triggering Mass Mutation Event!")
|
| 166 |
|
| 167 |
+
survivors = [x[2] for x in results[:6]]
|
| 168 |
new_pop = list(survivors)
|
| 169 |
|
|
|
|
| 170 |
while len(new_pop) < 30:
|
|
|
|
| 171 |
p1 = random.choice(survivors)
|
| 172 |
p2 = random.choice(survivors)
|
| 173 |
|
|
|
|
| 175 |
'lookback': p1['lookback'] if random.random() > 0.5 else p2['lookback'],
|
| 176 |
'top_n': p1['top_n'] if random.random() > 0.5 else p2['top_n'],
|
| 177 |
'rebalance': p1['rebalance'] if random.random() > 0.5 else p2['rebalance'],
|
| 178 |
+
'stop_loss': p1['stop_loss'] if random.random() > 0.5 else p2['stop_loss'],
|
| 179 |
+
'trend_filter': p1['trend_filter'] if random.random() > 0.5 else p2['trend_filter']
|
| 180 |
}
|
| 181 |
|
|
|
|
| 182 |
mutation_rate = 0.8 if stall_count >= 3 else 0.3
|
| 183 |
|
| 184 |
+
if random.random() < mutation_rate: child['lookback'] = random.choice([5, 10, 15, 20, 30, 45, 60])
|
| 185 |
+
if random.random() < mutation_rate: child['top_n'] = random.choice([2, 3, 4, 5])
|
| 186 |
+
if random.random() < mutation_rate: child['rebalance'] = random.choice([2, 3, 5, 7, 10])
|
| 187 |
+
if random.random() < mutation_rate: child['stop_loss'] = random.choice([0.02, 0.04, 0.06])
|
| 188 |
+
if random.random() < mutation_rate: child['trend_filter'] = random.choice([30, 50, 100, 200])
|
| 189 |
|
| 190 |
new_pop.append(child)
|
| 191 |
|
|
|
|
| 192 |
if stall_count >= 4:
|
| 193 |
stall_count = 0
|
| 194 |
|
|
|
|
| 202 |
|
| 203 |
if best_curve is not None:
|
| 204 |
plt.figure(figsize=(12, 7))
|
| 205 |
+
plt.plot(best_curve, label=f"Zero-Leverage AI Strategy ({best_cagr*100:.1f}%)", color='blue', linewidth=2)
|
| 206 |
|
| 207 |
nifty = data["^NSEI"]
|
| 208 |
bench = (nifty.loc[best_curve.index] / nifty.loc[best_curve.index[0]]) * INITIAL_CAPITAL
|
| 209 |
plt.plot(bench, label="Nifty 50 Index", color='gray', linestyle='--')
|
| 210 |
|
| 211 |
plt.yscale('log')
|
| 212 |
+
plt.title("Renaissance Pure Alpha Engine (0x Leverage / 100% Cash)")
|
| 213 |
plt.ylabel("Portfolio Value (Log Scale)")
|
| 214 |
plt.legend()
|
| 215 |
plt.grid(True, alpha=0.3)
|