Spaces:
Sleeping
Sleeping
File size: 11,287 Bytes
0198e22 9c009dc 8048f6a e5047cd 9c009dc 4162fed 0198e22 e5047cd ff98fe2 4162fed fe747fe 0198e22 e5047cd 0198e22 4162fed 0198e22 fe747fe 0198e22 0f6cbdf fe747fe 3144966 e949b63 3144966 8048f6a fe747fe e949b63 a171747 ebbb060 8048f6a fe747fe a171747 ebbb060 a171747 8048f6a fe747fe 4162fed fe747fe 3144966 fe747fe 8048f6a e949b63 fe747fe e949b63 8048f6a 3144966 05285a1 3144966 a171747 c637c2f a171747 0198e22 a171747 c637c2f 3144966 05285a1 3144966 05285a1 3144966 c637c2f e949b63 3144966 fe747fe 4162fed 3144966 4162fed fe747fe 98f3f06 51a991c 0198e22 fe747fe 3144966 0198e22 e949b63 51a991c ff98fe2 0198e22 ebbb060 3144966 e949b63 ebbb060 0198e22 c13b815 3144966 fe747fe e949b63 fe747fe ebbb060 a171747 fe747fe 4162fed c13b815 e949b63 4162fed fe747fe 0198e22 fe747fe 3144966 fe747fe 4162fed 3144966 c13b815 e949b63 0198e22 a171747 c13b815 05285a1 c637c2f e949b63 c13b815 05285a1 a171747 c13b815 ebbb060 05285a1 a171747 c13b815 e949b63 3144966 c13b815 e949b63 4162fed fe747fe ff98fe2 3144966 0198e22 e949b63 3144966 0198e22 e949b63 3144966 e949b63 3144966 ff98fe2 e949b63 ff98fe2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | import warnings
warnings.filterwarnings("ignore")
import matplotlib
matplotlib.use('Agg')
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os
import time
import random
import json
# Silence Pandas Future Warnings
pd.options.mode.chained_assignment = None
try: pd.set_option('future.no_silent_downcasting', True)
except: pass
# --- CONFIGURATION ---
START_DATE = "2010-01-01"
INITIAL_CAPITAL = 1000000
SIMULATION_TIME_MIN = 35
CACHE_FILE = "fundamental_cache.json"
def get_all_csv_tickers():
try:
df = pd.read_csv("EQUITY_L.csv")
df.columns = [c.strip() for c in df.columns]
if 'SERIES' in df.columns: df = df[df['SERIES'] == 'EQ']
tickers = [f"{x}.NS" for x in df['SYMBOL'].tolist()]
return tickers
except:
return ["RELIANCE.NS", "TCS.NS", "INFY.NS", "HDFCBANK.NS"]
def fundamental_deep_scan(tickers):
print(f"🔍 PHASE 1: Deep Fundamental Scan of {len(tickers)} stocks...")
print("⏳ This will take 30-45 minutes. It will only happen ONCE and save to cache.")
scored_stocks = []
count = 0
for ticker in tickers:
count += 1
if count % 50 == 0:
print(f" -> Scanned {count}/{len(tickers)} stocks...")
try:
stock = yf.Ticker(ticker)
info = stock.info
roe = info.get('returnOnEquity', 0) or 0
pe = info.get('trailingPE', 0) or 1000
growth = info.get('revenueGrowth', 0) or 0
score = 0
if roe > 0.15: score += 40
if growth > 0.10: score += 30
if 0 < pe < 60: score += 30
# Keep companies with strong actual business fundamentals
if score >= 40:
scored_stocks.append({'ticker': ticker, 'score': score})
except Exception:
pass
# Delay to prevent IP Ban from Yahoo Finance
time.sleep(random.uniform(0.1, 0.4))
scored_stocks.sort(key=lambda x: x['score'], reverse=True)
# We take the top 250 fundamentally strongest companies
elite_tickers = [x['ticker'] for x in scored_stocks[:250]]
with open(CACHE_FILE, 'w') as f:
json.dump(elite_tickers, f)
print(f"✅ Phase 1 Complete. Saved {len(elite_tickers)} Elite Stocks to cache.")
return elite_tickers
def load_fundamental_universe():
if os.path.exists(CACHE_FILE):
print("📂 Loading Fundamentally Strong Universe from Cache...")
with open(CACHE_FILE, 'r') as f:
return json.load(f)
else:
all_tickers = get_all_csv_tickers()
return fundamental_deep_scan(all_tickers)
def run_strategy_genome(data, genome):
if data.empty: return -1.0, []
nifty = data.get("^NSEI")
gold = data.get("GC=F")
if nifty is None or gold is None: return -1.0, []
stock_cols = [c for c in data.columns if c not in ["^NSEI", "GC=F"]]
stocks = data[stock_cols]
lookback = int(genome['lookback'])
top_n = int(genome['top_n'])
rebalance_days = int(genome['rebalance'])
stop_loss = float(genome['stop_loss'])
trend_filter = int(genome['trend_filter'])
max_vol = float(genome['max_vol'])
momentum = stocks.pct_change(lookback)
daily_returns = stocks.pct_change(1)
volatility = daily_returns.rolling(lookback).std() * np.sqrt(252)
nifty_ma = nifty.rolling(trend_filter).mean()
curve = [INITIAL_CAPITAL]
curr_val = INITIAL_CAPITAL
dates = stocks.index
if len(dates) < 260: return -0.5, []
sim_dates = [dates[252]]
for i in range(252, len(dates)-1, rebalance_days):
curr = dates[i]
nxt = dates[min(i+rebalance_days, len(dates)-1)]
try:
is_bull = nifty.loc[curr] > nifty_ma.loc[curr]
period_ret = 0.0
if is_bull:
if curr in momentum.index and curr in volatility.index:
scores = momentum.loc[curr]
vols = volatility.loc[curr]
# MICRO-CAP UNLOCKED: Lowered to ₹10 to catch real fundamental turnarounds.
# (Since Phase 1 ensures they have >15% ROE, a ₹12 stock here is a true hidden gem, not a scam)
valid_prices = stocks.loc[curr] > 10.0
# Must be fundamentally strong (by universe), positive momentum, low vol, NOT a fractional penny stock
valid_stocks = scores[(scores > 0) & (vols < max_vol) & valid_prices]
picks = valid_stocks.sort_values(ascending=False).head(top_n).index.tolist()
if len(picks) > 0:
p1 = stocks.loc[curr, picks]
p2 = stocks.loc[nxt, picks]
stock_ret = ((p2 - p1) / p1).mean()
if pd.isna(stock_ret): stock_ret = 0.0
period_ret = stock_ret
else:
g_ret = (gold.loc[nxt] - gold.loc[curr]) / gold.loc[curr]
if pd.isna(g_ret): g_ret = 0.0
period_ret = g_ret
if period_ret < -stop_loss: period_ret = -stop_loss
curr_val = curr_val * (1 + period_ret)
if curr_val < 0: curr_val = 0
curve.append(curr_val)
sim_dates.append(nxt)
except:
continue
if not curve: return -1.0, []
final = curve[-1]
years = (sim_dates[-1] - sim_dates[0]).days / 365.25
cagr = (final / INITIAL_CAPITAL) ** (1/years) - 1 if years > 0 else 0
return cagr, pd.Series(curve, index=sim_dates)
def backtest_engine():
print(f"⚙️ Initializing Phase 2: AI Genetic Backtest...")
start_time = time.time()
tickers = load_fundamental_universe()
tickers += ["^NSEI", "GC=F"]
try:
print(f"🌍 Fetching 16-Year History for Elite Universe...")
data = yf.download(tickers, start=START_DATE, progress=False, threads=True)
if isinstance(data.columns, pd.MultiIndex):
try: data = data['Close']
except: pass
data = data.ffill().bfill().infer_objects(copy=False)
if data.empty: return None
population = []
for _ in range(30):
population.append({
'lookback': random.choice([10, 20, 30, 45, 60, 90]),
'top_n': random.choice([5, 6, 7, 8, 9, 10]),
'rebalance': random.choice([3, 5, 7, 10, 14]),
'stop_loss': random.choice([0.02, 0.04, 0.06, 0.08]),
'trend_filter': random.choice([30, 50, 100, 200]),
'max_vol': random.choice([0.30, 0.40, 0.50, 0.60, 0.80])
})
best_cagr = -1.0
best_curve = None
stall_count = 0
generation = 1
while (time.time() - start_time) < (SIMULATION_TIME_MIN * 60):
print(f"\n🧬 Gen {generation}: Testing 1.0x Portfolios (Strict Fundamentals + Price > ₹10)")
results = []
for genome in population:
cagr, curve = run_strategy_genome(data, genome)
results.append((cagr, curve, genome))
results.sort(key=lambda x: x[0], reverse=True)
if results:
current_top_cagr = results[0][0]
if current_top_cagr > best_cagr + 0.001:
best_cagr = current_top_cagr
best_curve = results[0][1]
best_dna = results[0][2]
stall_count = 0
else:
stall_count += 1
print(f" 🏆 16-Year Average CAGR: {best_cagr*100:.1f}%")
print(f" 🧬 DNA: {best_dna['top_n']} Stocks | Bal: {best_dna['rebalance']}d | Regime: {best_dna['trend_filter']}d | Vol Cap: {best_dna['max_vol']*100}%")
survivors = [x[2] for x in results[:6]]
new_pop = list(survivors)
while len(new_pop) < 30:
p1 = random.choice(survivors)
p2 = random.choice(survivors)
child = {
'lookback': p1['lookback'] if random.random() > 0.5 else p2['lookback'],
'top_n': p1['top_n'] if random.random() > 0.5 else p2['top_n'],
'rebalance': p1['rebalance'] if random.random() > 0.5 else p2['rebalance'],
'stop_loss': p1['stop_loss'] if random.random() > 0.5 else p2['stop_loss'],
'trend_filter': p1['trend_filter'] if random.random() > 0.5 else p2['trend_filter'],
'max_vol': p1['max_vol'] if random.random() > 0.5 else p2['max_vol']
}
mutation_rate = 0.8 if stall_count >= 3 else 0.3
if random.random() < mutation_rate: child['lookback'] = random.choice([10, 20, 30, 45, 60, 90])
if random.random() < mutation_rate: child['top_n'] = random.choice([5, 6, 7, 8, 9, 10])
if random.random() < mutation_rate: child['rebalance'] = random.choice([3, 5, 7, 10, 14])
if random.random() < mutation_rate: child['stop_loss'] = random.choice([0.02, 0.04, 0.06, 0.08])
if random.random() < mutation_rate: child['trend_filter'] = random.choice([30, 50, 100, 200])
if random.random() < mutation_rate: child['max_vol'] = random.choice([0.30, 0.40, 0.50, 0.60, 0.80])
new_pop.append(child)
if stall_count >= 4:
stall_count = 0
population = new_pop
generation += 1
time.sleep(1)
output_file = "backtest_result.png"
if os.path.exists(output_file): os.remove(output_file)
if best_curve is not None:
plt.figure(figsize=(12, 7))
plt.plot(best_curve, label=f"Fundamentally Strong Strategy ({best_cagr*100:.1f}%)", color='blue', linewidth=2)
nifty = data["^NSEI"]
bench = (nifty.loc[best_curve.index] / nifty.loc[best_curve.index[0]]) * INITIAL_CAPITAL
plt.plot(bench, label="Nifty 50 Index", color='gray', linestyle='--')
plt.yscale('log')
plt.title("Renaissance Engine: Quality Momentum (Zero-Leverage, 5-10 Stocks, ₹10+ Floor)")
plt.ylabel("Portfolio Value (Log Scale)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig(output_file)
plt.close()
return output_file
return None
except Exception as e:
print(f"❌ Error: {e}")
return None
if __name__ == "__main__":
backtest_engine() |