mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
refactor: remove all proprietary terms from codebase and git history
- Rename FTMO_* constants → generic names (RISK_PER_TRADE, MAX_DAILY_LOSS, etc.) - Rename backtest_signal_ftmo → backtest_signal_risk - Rename _apply_ftmo_mask → _apply_risk_mask - Clean all FTMO/riskMgmt mentions from commit messages via filter-branch - AGENTS.md: add non-negotiable rule — NEVER mention proprietary terms in commits/releases - Code variables and function names sanitized project-wide - Force-pushed rewritten history to remote
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import json, numpy as np, pandas as pd
|
||||
from pathlib import Path
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
close = pd.read_hdf("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5", key="data")["$close"]
|
||||
close = close.droplevel(-1).sort_index().dropna().resample("1h").last().dropna()
|
||||
@@ -34,7 +34,7 @@ for i, f in enumerate(factors[:100]):
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=close.index)
|
||||
sig[~is_session] = 0
|
||||
if sig.abs().sum() < 20: continue
|
||||
r = backtest_signal_ftmo(close, sig.fillna(0), txn_cost_bps=2.14)
|
||||
r = backtest_signal_risk(close, sig.fillna(0), txn_cost_bps=2.14)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
results.append((f"{f['name']}_{label}", oos, oos_m, r.get("oos_n_trades",0)))
|
||||
@@ -67,7 +67,7 @@ if top:
|
||||
df = pd.DataFrame(all_sig, index=close.index).fillna(0)
|
||||
for n in [3, 5, 8]:
|
||||
combo = df[list(df.columns)[:n]].mean(axis=1)
|
||||
r = backtest_signal_ftmo(close, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
r = backtest_signal_risk(close, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos_m = r.get("oos_monthly_return_pct",0) or 0
|
||||
dd = (r.get("oos_max_drawdown",0) or 0)*100
|
||||
ann = ((1+oos_m/100)**12-1)*100
|
||||
|
||||
@@ -18,7 +18,7 @@ import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
@@ -71,7 +71,7 @@ def backtest(signal, close, label="") -> dict:
|
||||
if signal is None or len(signal) < 100:
|
||||
return {"wf_sharpe": -999, "oos_sharpe": -999, "oos_monthly": 0, "oos_dd": 0, "trades": 0}
|
||||
common = close.index.intersection(signal.dropna().index)
|
||||
r = backtest_signal_ftmo(close.loc[common], signal.reindex(common).fillna(0),
|
||||
r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0),
|
||||
txn_cost_bps=TXN_COST_BPS, wf_rolling=False)
|
||||
oos = r.get("oos_sharpe", -999)
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""30min Full Factor Scan — find all profitable signals."""
|
||||
import json, numpy as np, pandas as pd
|
||||
from pathlib import Path
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
c = pd.read_hdf("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5", key="data")["$close"]
|
||||
c = c.droplevel(-1).sort_index().dropna().resample("30min").last().dropna()
|
||||
@@ -33,7 +33,7 @@ for i, f in enumerate(factors[:200]):
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=c.index)
|
||||
sig[~is_s] = 0
|
||||
if sig.abs().sum() < 20: continue
|
||||
r = backtest_signal_ftmo(c, sig.fillna(0), txn_cost_bps=2.14)
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
if oos_m > 0.2:
|
||||
@@ -72,7 +72,7 @@ if results:
|
||||
print(f"\n=== COMBO TESTS ===")
|
||||
for n in [2, 3, 5, 8, len(cols)]:
|
||||
combo = df[cols[:n]].mean(axis=1)
|
||||
r = backtest_signal_ftmo(c, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
r = backtest_signal_risk(c, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
dd = (r.get("oos_max_drawdown", 0) or 0) * 100
|
||||
t = r.get("oos_n_trades", 0)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Add FTMO-compliant risk management to existing strategies.
|
||||
Add RiskMgmt-compliant risk management to existing strategies.
|
||||
|
||||
For each accepted strategy, add:
|
||||
- Stop Loss: 2%
|
||||
@@ -27,11 +27,11 @@ console = Console()
|
||||
STRATEGIES_DIR = Path('results/strategies_new')
|
||||
OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
|
||||
# FTMO Risk Parameters
|
||||
# RiskMgmt Risk Parameters
|
||||
STOP_LOSS = 0.02 # 2% hard stop
|
||||
TAKE_PROFIT = 0.04 # 4% target (2x SL)
|
||||
TRAILING_STOP = 0.015 # 1.5% trail after 2% profit
|
||||
MAX_DAILY_LOSS = 0.05 # 5% FTMO daily limit
|
||||
MAX_DAILY_LOSS = 0.05 # 5% RiskMgmt daily limit
|
||||
|
||||
def load_ohlcv():
|
||||
"""Load OHLCV close prices."""
|
||||
@@ -147,11 +147,11 @@ def evaluate_strategy(strategy_returns, signal_aligned):
|
||||
'n_bars': int(n_bars),
|
||||
'n_months': float(n_months),
|
||||
'max_daily_loss': float(max_daily_loss),
|
||||
'ftmo_compliant': max_daily_loss <= MAX_DAILY_LOSS and max_dd > -0.10,
|
||||
'riskmgmt_compliant': max_daily_loss <= MAX_DAILY_LOSS and max_dd > -0.10,
|
||||
}
|
||||
|
||||
def main():
|
||||
console.print("[bold cyan]🔒 Adding FTMO Risk Management to Existing Strategies[/bold cyan]\n")
|
||||
console.print("[bold cyan]🔒 Adding RiskMgmt Risk Management to Existing Strategies[/bold cyan]\n")
|
||||
|
||||
# Load OHLCV
|
||||
console.print("📊 Loading OHLCV data...")
|
||||
@@ -254,7 +254,7 @@ def main():
|
||||
'new_trades': metrics['n_trades'],
|
||||
'new_monthly_ret': metrics['monthly_return_pct'],
|
||||
'max_daily_loss': metrics['max_daily_loss'],
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
}
|
||||
results.append(result)
|
||||
|
||||
@@ -265,7 +265,7 @@ def main():
|
||||
'trailing_stop': TRAILING_STOP,
|
||||
'trailing_trigger': 0.02,
|
||||
'max_daily_loss': MAX_DAILY_LOSS,
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
}
|
||||
data['evaluated_with_risk_mgmt'] = metrics
|
||||
data['summary'] = {
|
||||
@@ -275,7 +275,7 @@ def main():
|
||||
'monthly_return_pct': metrics['monthly_return_pct'],
|
||||
'real_ic': metrics['ic'],
|
||||
'real_n_trades': metrics['n_trades'],
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
'forward_bars': 12,
|
||||
'trading_style': 'daytrading',
|
||||
}
|
||||
@@ -296,7 +296,7 @@ def main():
|
||||
# Display results
|
||||
console.print("\n[bold green]✓ All strategies processed![/bold green]\n")
|
||||
|
||||
table = Table(title="📊 FTMO Risk Management Results")
|
||||
table = Table(title="📊 RiskMgmt Risk Management Results")
|
||||
table.add_column("#", justify="right")
|
||||
table.add_column("Strategy", style="cyan")
|
||||
table.add_column("IC", justify="right")
|
||||
@@ -304,11 +304,11 @@ def main():
|
||||
table.add_column("Trades", justify="right")
|
||||
table.add_column("Monthly %", justify="right")
|
||||
table.add_column("Max DD", justify="right")
|
||||
table.add_column("FTMO", justify="center")
|
||||
table.add_column("RiskMgmt", justify="center")
|
||||
|
||||
results.sort(key=lambda x: x['new_sharpe'], reverse=True)
|
||||
for i, r in enumerate(results, 1):
|
||||
ftmo = "✅" if r['ftmo_compliant'] else "❌"
|
||||
riskmgmt = "✅" if r['riskmgmt_compliant'] else "❌"
|
||||
table.add_row(
|
||||
str(i), r['name'],
|
||||
f"{r['new_ic']:.4f}",
|
||||
@@ -316,14 +316,14 @@ def main():
|
||||
str(r['new_trades']),
|
||||
f"{r['new_monthly_ret']:.2f}%",
|
||||
f"{r['new_max_dd']:.1%}",
|
||||
ftmo
|
||||
riskmgmt
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Summary
|
||||
ftmo_count = sum(1 for r in results if r['ftmo_compliant'])
|
||||
console.print(f"\n[bold]FTMO-Compliant:[/bold] {ftmo_count}/{len(results)} strategies")
|
||||
riskmgmt_count = sum(1 for r in results if r['riskmgmt_compliant'])
|
||||
console.print(f"\n[bold]RiskMgmt-Compliant:[/bold] {riskmgmt_count}/{len(results)} strategies")
|
||||
|
||||
if results:
|
||||
best = results[0]
|
||||
@@ -331,7 +331,7 @@ def main():
|
||||
console.print(f" Sharpe: {best['new_sharpe']:.2f}")
|
||||
console.print(f" Monthly Return: {best['new_monthly_ret']:.2f}%")
|
||||
console.print(f" Max Drawdown: {best['new_max_dd']:.1%}")
|
||||
console.print(f" FTMO Compliant: {'✅' if best['ftmo_compliant'] else '❌'}")
|
||||
console.print(f" RiskMgmt Compliant: {'✅' if best['riskmgmt_compliant'] else '❌'}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -68,8 +68,8 @@ def build_ml_model(factor_values: pd.DataFrame, close: pd.Series, style: str) ->
|
||||
signal = pd.Series(np.sign(preds), index=common[split:])
|
||||
|
||||
# Backtest
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
bt = backtest_signal_ftmo(
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
bt = backtest_signal_risk(
|
||||
close=close_aligned.loc[common[split:]],
|
||||
signal=signal,
|
||||
txn_cost_bps=2.14,
|
||||
|
||||
@@ -9,7 +9,7 @@ Usage:
|
||||
# Swing trading (96-bar forward returns)
|
||||
python nexquant_gen_strategies_real_bt.py 10
|
||||
|
||||
# Daytrading with FTMO constraints (12-bar forward returns)
|
||||
# Daytrading with RiskMgmt constraints (12-bar forward returns)
|
||||
TRADING_STYLE=daytrading python nexquant_gen_strategies_real_bt.py 5
|
||||
|
||||
# With parallel workers (default: CPU count)
|
||||
@@ -66,7 +66,7 @@ if TRADING_STYLE == "daytrading":
|
||||
MAX_DRAWDOWN = -0.10
|
||||
MIN_MONTHLY_RETURN_PCT = 15.0
|
||||
STYLE_EMOJI = "🎯 Daytrading"
|
||||
STYLE_DESC = "short-term intraday with FTMO compliance"
|
||||
STYLE_DESC = "short-term intraday with RiskMgmt compliance"
|
||||
else:
|
||||
FORWARD_BARS = int(os.getenv("FORWARD_BARS", "96"))
|
||||
MIN_IC = 0.02
|
||||
@@ -229,7 +229,7 @@ Hard requirements:
|
||||
- Use causal indicators only: rolling windows, shift(1) — NO look-ahead bias
|
||||
- No factor data — compute everything from 'close'
|
||||
- Keep it simple: 2-3 indicators max
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after FTMO costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
|
||||
elif TRADING_STYLE == "daytrading":
|
||||
system_prompt = f"""You are an expert daytrading quant specializing in EUR/USD scalping and intraday strategies.
|
||||
@@ -258,7 +258,7 @@ Hard requirements:
|
||||
- Use rolling z-scores with windows of 5-20 bars (not 50-100), thresholds ±0.2 to ±0.5
|
||||
- Combine 2 factors: one momentum, one mean-reversion
|
||||
- NO global mean/std — always use rolling(window).mean() with shift(1) to avoid look-ahead bias
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after FTMO costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
|
||||
else:
|
||||
system_prompt = f"""You are a quantitative trading expert specializing in EUR/USD daily swing strategies.
|
||||
@@ -289,7 +289,7 @@ Output ONLY valid JSON with these fields:
|
||||
|
||||
{f'Previous feedback: {feedback}' if feedback else 'First attempt - be creative!'}
|
||||
|
||||
Use daily-level signal logic (factor above/below rolling daily mean). Signal changes once per day. TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after FTMO costs (2.35 pip/trade)."""
|
||||
Use daily-level signal logic (factor above/below rolling daily mean). Signal changes once per day. TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade)."""
|
||||
|
||||
api = APIBackend()
|
||||
response = api.build_messages_and_create_chat_completion(
|
||||
@@ -376,8 +376,8 @@ signal.fillna(0).to_pickle('signal.pkl')
|
||||
except Exception as e:
|
||||
return {"status": "failed", "reason": str(e)[:200]}
|
||||
|
||||
# Main process: FTMO-realistic backtest (leverage + daily/total loss limits).
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
# Main process: RiskMgmt-realistic backtest (leverage + daily/total loss limits).
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
common = close.index.intersection(signal.index)
|
||||
if len(common) < 100:
|
||||
@@ -388,7 +388,7 @@ signal.fillna(0).to_pickle('signal.pkl')
|
||||
fwd_returns = close_a.pct_change(FORWARD_BARS).shift(-FORWARD_BARS)
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import OOS_START_DEFAULT
|
||||
return backtest_signal_ftmo(
|
||||
return backtest_signal_risk(
|
||||
close=close_a,
|
||||
signal=signal_a,
|
||||
txn_cost_bps=TXN_COST_BPS,
|
||||
@@ -615,7 +615,7 @@ def main(target_count=10):
|
||||
"n_bars": bt_result.get("n_bars", 0), "n_months": bt_result.get("n_months", 0),
|
||||
"trading_style": TRADING_STYLE,
|
||||
"ohlcv_only": OHLCV_ONLY,
|
||||
"engine": "ftmo_v2",
|
||||
"engine": "riskmgmt_v2",
|
||||
"txn_cost_bps": TXN_COST_BPS,
|
||||
# Walk-forward OOS split
|
||||
"oos_sharpe": bt_result.get("oos_sharpe"),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Grid-Search Strategy Generator — no LLM, deterministic, FTMO-verified.
|
||||
"""Grid-Search Strategy Generator — no LLM, deterministic, RiskMgmt-verified.
|
||||
|
||||
Core idea: Instead of LLM-generated code, use a fixed signal template and
|
||||
grid-search the parameters. Factors are aligned to daily resolution (where
|
||||
they have actual predictive power), signal is forward-filled to 1-min for
|
||||
FTMO backtest execution.
|
||||
RiskMgmt backtest execution.
|
||||
|
||||
Template: z-score → IC-weighted composite → asymmetric thresholds → signal
|
||||
"""
|
||||
@@ -29,7 +29,7 @@ OHLCV_PATH = Path(
|
||||
)
|
||||
|
||||
# ── Target ───────────────────────────────────────────────────────────────────
|
||||
MIN_MONTHLY_RETURN_PCT = 1.0 # Raw backtest target (FTMO will reduce ~50%)
|
||||
MIN_MONTHLY_RETURN_PCT = 1.0 # Raw backtest target (RiskMgmt will reduce ~50%)
|
||||
MIN_SHARPE = 0.5
|
||||
MAX_DRAWDOWN = -0.30
|
||||
MIN_WIN_RATE = 0.35
|
||||
@@ -175,7 +175,7 @@ def evaluate_one(args: tuple) -> dict | None:
|
||||
# Forward-fill to 1-min for backtest
|
||||
signal_1min = daily_signal.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
# Fast backtest (no FTMO mask, no walk-forward — <1s per eval)
|
||||
# Fast backtest (no RiskMgmt mask, no walk-forward — <1s per eval)
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
bt = backtest_signal(
|
||||
|
||||
@@ -13,7 +13,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
@@ -63,7 +63,7 @@ def backtest(signal) -> float:
|
||||
common = close.index.intersection(signal.dropna().index)
|
||||
if len(common) < 100:
|
||||
return -999
|
||||
r = backtest_signal_ftmo(close.loc[common], signal.reindex(common).fillna(0),
|
||||
r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0),
|
||||
txn_cost_bps=TXN_COST_BPS, wf_rolling=False)
|
||||
return r.get("oos_sharpe", -999)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.model_selection import TimeSeriesSplit
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
@@ -98,7 +98,7 @@ def make_target(c: pd.Series, horizon: int = 5) -> np.ndarray:
|
||||
def backtest_metric(c, y_pred, split_idx):
|
||||
test_c = c.iloc[split_idx:]
|
||||
sig = pd.Series(y_pred[split_idx:len(test_c)+split_idx], index=test_c.index[:len(y_pred)-split_idx])
|
||||
r = backtest_signal_ftmo(test_c.iloc[:len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
r = backtest_signal_risk(test_c.iloc[:len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
return r.get("oos_sharpe", -999) or -999
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ def main():
|
||||
model.fit(X[:split_idx], y_vals[:split_idx])
|
||||
y_pred = model.predict(X)
|
||||
sig = pd.Series(y_pred[split_idx:len(c)-split_idx+split_idx], index=c.index[split_idx:split_idx+len(y_pred)-split_idx])
|
||||
r = backtest_signal_ftmo(c.iloc[split_idx:split_idx+len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
r = backtest_signal_risk(c.iloc[split_idx:split_idx+len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
|
||||
oos_s = r.get("oos_sharpe", -999)
|
||||
oos_m = (r.get("oos_monthly_return_pct", 0) or 0)
|
||||
|
||||
@@ -77,7 +77,7 @@ def main():
|
||||
print(" Quick Daily Strategy Test on Multi-Asset")
|
||||
print(f"{'='*60}")
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
for asset in df.columns:
|
||||
c = df[asset].dropna()
|
||||
@@ -91,7 +91,7 @@ def main():
|
||||
sig[f > s] = 1
|
||||
sig[f < s] = -1
|
||||
|
||||
r = backtest_signal_ftmo(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
status = "✅" if oos > 0 else " "
|
||||
@@ -106,7 +106,7 @@ def main():
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
sig[f > s] = 1
|
||||
sig[f < s] = -1
|
||||
r = backtest_signal_ftmo(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
print(f" SMA10/30 extended: OOS={oos:+8.2f} Mon={r.get('oos_monthly_return_pct',0):+.2f}%")
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA = Path("git_ignore_folder/factor_implementation_source_data/multi_asset_daily.h5")
|
||||
|
||||
@@ -85,7 +85,7 @@ def main():
|
||||
sig_func = STRATEGIES.get(name, lambda c: rsi_signal(c, 21, 25, 75))
|
||||
sig = sig_func(c).fillna(0)
|
||||
|
||||
r = backtest_signal_ftmo(c, sig, txn_cost_bps=2.14, wf_rolling=True)
|
||||
r = backtest_signal_risk(c, sig, txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
status = "✅" if oos > 0 else " "
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Given N strategies with daily returns, find the optimal combination that:
|
||||
- Maximizes monthly return
|
||||
- Keeps max drawdown within FTMO limits (10% total, 5% daily)
|
||||
- Keeps max drawdown within RiskMgmt limits (10% total, 5% daily)
|
||||
- Diversifies across uncorrelated strategies
|
||||
"""
|
||||
|
||||
@@ -23,8 +23,8 @@ OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
|
||||
TARGET_MONTHLY = 15.0
|
||||
MAX_DD = 0.10 # FTMO: 10% max total drawdown
|
||||
MAX_DAILY_DD = 0.05 # FTMO: 5% max daily drawdown
|
||||
MAX_DD = 0.10 # RiskMgmt: 10% max total drawdown
|
||||
MAX_DAILY_DD = 0.05 # RiskMgmt: 5% max daily drawdown
|
||||
MIN_TRADES = 30
|
||||
MIN_SHARPE = 0.5
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ FACTOR_FILES = Path('results/factors')
|
||||
VALUE_FILES = FACTOR_FILES / 'values'
|
||||
OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
|
||||
# Best daytrading strategies (12-min horizon, optimized for FTMO)
|
||||
# Best daytrading strategies (12-min horizon, optimized for RiskMgmt)
|
||||
DAYTRADING_COMBOS = [
|
||||
{
|
||||
'name': 'MomentumDivergence12min',
|
||||
@@ -236,7 +236,7 @@ def load_factor_series(name):
|
||||
def main(n_strategies=5):
|
||||
console.print("[bold cyan]🎯 Daytrading Strategy Generator (Quick Mode)[/bold cyan]\n")
|
||||
console.print(" Style: 12-minute forward returns")
|
||||
console.print(" Target: FTMO compliant (IC>0.02, Sharpe>0.5, Trades>20, DD>-10%)\n")
|
||||
console.print(" Target: RiskMgmt compliant (IC>0.02, Sharpe>0.5, Trades>20, DD>-10%)\n")
|
||||
|
||||
# Load OHLCV data
|
||||
if not OHLCV_PATH.exists():
|
||||
@@ -422,7 +422,7 @@ print(json.dumps(result))
|
||||
trades = result.get('n_trades', 0)
|
||||
dd = result.get('max_drawdown', 0)
|
||||
|
||||
# FTMO criteria
|
||||
# RiskMgmt criteria
|
||||
if abs(ic) > 0.02 and sharpe > 0.5 and trades > 20 and dd > -0.10:
|
||||
strategy = {
|
||||
'strategy_name': combo['name'],
|
||||
|
||||
@@ -36,7 +36,7 @@ from rich.console import Console
|
||||
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo # noqa: E402
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk # noqa: E402
|
||||
|
||||
OHLCV_PATH = Path("/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_VALUES_DIR = Path("/home/nico/NexQuant/results/factors/values")
|
||||
@@ -184,7 +184,7 @@ def rebacktest_one(
|
||||
# Signal can arrive on either the factor index or the close index.
|
||||
signal = signal.reindex(close_a.index).ffill().fillna(0)
|
||||
|
||||
result = backtest_signal_ftmo(
|
||||
result = backtest_signal_risk(
|
||||
close=close_a,
|
||||
signal=signal,
|
||||
txn_cost_bps=txn_cost_bps,
|
||||
@@ -252,10 +252,10 @@ def main() -> None:
|
||||
"real_n_trades": bt.get("n_trades"),
|
||||
"total_return": bt.get("total_return"),
|
||||
"annualized_return": bt.get("annualized_return"),
|
||||
"ftmo_daily_loss_hit": bt.get("ftmo_daily_loss_hit"),
|
||||
"ftmo_total_loss_hit": bt.get("ftmo_total_loss_hit"),
|
||||
"riskmgmt_daily_loss_hit": bt.get("riskmgmt_daily_loss_hit"),
|
||||
"riskmgmt_total_loss_hit": bt.get("riskmgmt_total_loss_hit"),
|
||||
"trading_style": data.get("summary", {}).get("trading_style"),
|
||||
"engine": "ftmo_v2",
|
||||
"engine": "riskmgmt_v2",
|
||||
"txn_cost_bps": args.txn_cost_bps,
|
||||
# Walk-forward OOS
|
||||
"is_sharpe": bt.get("is_sharpe"),
|
||||
@@ -280,7 +280,7 @@ def main() -> None:
|
||||
data["max_drawdown"] = bt.get("max_drawdown")
|
||||
data["win_rate"] = bt.get("win_rate")
|
||||
data["total_return"] = bt.get("total_return")
|
||||
data["reevaluation_status"] = "ftmo_v2"
|
||||
data["reevaluation_status"] = "riskmgmt_v2"
|
||||
try:
|
||||
import json as _json
|
||||
f.write_text(_json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Smart Strategy Generation with Feedback Loop, Parameter Optimization & FTMO Risk Management.
|
||||
Smart Strategy Generation with Feedback Loop, Parameter Optimization & RiskMgmt Risk Management.
|
||||
|
||||
Generates EUR/USD daytrading strategies using LLM with:
|
||||
- Adaptive feedback loop (IC, trades, drawdown-based suggestions)
|
||||
- Grid search for optimal parameters (thresholds, SL/TP, trailing stops)
|
||||
- Mandatory FTMO-compliant risk management layer
|
||||
- Mandatory RiskMgmt-compliant risk management layer
|
||||
- Comprehensive evaluation metrics # nosec
|
||||
|
||||
Usage:
|
||||
@@ -62,11 +62,11 @@ logger = logging.getLogger("SmartStrategyGen")
|
||||
console = Console()
|
||||
|
||||
# ============================================================================
|
||||
# FTMO Risk Management Constants
|
||||
# RiskMgmt Risk Management Constants
|
||||
# ============================================================================
|
||||
class FTMORiskLimits:
|
||||
"""FTMO-compliant risk management constants."""
|
||||
MAX_DAILY_LOSS_PCT = 0.05 # 5% max daily loss (FTMO rule)
|
||||
class RiskMgmtRiskLimits:
|
||||
"""RiskMgmt-compliant risk management constants."""
|
||||
MAX_DAILY_LOSS_PCT = 0.05 # 5% max daily loss (RiskMgmt rule)
|
||||
MAX_PER_TRADE_LOSS_PCT = 0.02 # 2% max per trade
|
||||
MAX_TOTAL_DRAWDOWN = 0.10 # 10% max overall drawdown
|
||||
MAX_POSITIONS = 1 # Only 1 position at a time
|
||||
@@ -103,7 +103,7 @@ ACCEPTANCE_CRITERIA = {
|
||||
PARAMETER_GRID = {
|
||||
"threshold_entry": [0.2, 0.3, 0.4, 0.5],
|
||||
"rolling_window": [10, 20, 30, 60],
|
||||
"stop_loss": [0.01, 0.015, 0.02], # 1%, 1.5%, 2% (HARD MAX: 2% for FTMO)
|
||||
"stop_loss": [0.01, 0.015, 0.02], # 1%, 1.5%, 2% (HARD MAX: 2% for RiskMgmt)
|
||||
"take_profit": [0.02, 0.03, 0.04, 0.06], # 2x-3x SL
|
||||
"trailing_stop": [0.01, 0.015], # 1%, 1.5% after profit threshold
|
||||
"trailing_activation": [0.015, 0.02], # Activate trail after 1.5%, 2% profit
|
||||
@@ -229,7 +229,7 @@ def setup_llm_env():
|
||||
# ============================================================================
|
||||
class RiskManagementEngine:
|
||||
"""
|
||||
FTMO-compliant risk management layer.
|
||||
RiskMgmt-compliant risk management layer.
|
||||
|
||||
Applies stop loss, take profit, trailing stop, and daily loss limits
|
||||
to strategy returns.
|
||||
@@ -262,13 +262,13 @@ class RiskManagementEngine:
|
||||
max_positions : int
|
||||
Maximum concurrent positions (default 1)
|
||||
"""
|
||||
# Validate FTMO compliance
|
||||
# Validate RiskMgmt compliance
|
||||
if stop_loss > 0.02:
|
||||
raise ValueError(f"Stop loss {stop_loss:.2%} exceeds FTMO max of 2%")
|
||||
raise ValueError(f"Stop loss {stop_loss:.2%} exceeds RiskMgmt max of 2%")
|
||||
if take_profit < stop_loss * 2:
|
||||
raise ValueError(f"Take profit {take_profit:.2%} must be at least 2x SL ({stop_loss*2:.2%})")
|
||||
if max_daily_loss > 0.05:
|
||||
raise ValueError(f"Daily loss {max_daily_loss:.2%} exceeds FTMO max of 5%")
|
||||
raise ValueError(f"Daily loss {max_daily_loss:.2%} exceeds RiskMgmt max of 5%")
|
||||
|
||||
self.stop_loss = stop_loss
|
||||
self.take_profit = take_profit
|
||||
@@ -411,7 +411,7 @@ class RiskManagementEngine:
|
||||
# ============================================================================
|
||||
class StrategyEvaluator:
|
||||
"""
|
||||
Comprehensive strategy evaluation with FTMO metrics. # nosec
|
||||
Comprehensive strategy evaluation with RiskMgmt metrics. # nosec
|
||||
"""
|
||||
|
||||
def __init__(self, trading_style: str = "daytrading", forward_bars: int = 96):
|
||||
@@ -495,7 +495,7 @@ class StrategyEvaluator:
|
||||
active_returns = strategy_returns[strategy_returns != 0]
|
||||
win_rate = (active_returns > 0).sum() / len(active_returns) if len(active_returns) > 0 else 0.0
|
||||
|
||||
# Daily loss analysis (for FTMO compliance)
|
||||
# Daily loss analysis (for RiskMgmt compliance)
|
||||
daily_returns = strategy_returns.groupby(
|
||||
strategy_returns.index.date if hasattr(strategy_returns.index[0], "date") else strategy_returns.index,
|
||||
).sum()
|
||||
@@ -533,9 +533,9 @@ class StrategyEvaluator:
|
||||
"n_bars": total_bars,
|
||||
"n_months": float(n_months),
|
||||
|
||||
# FTMO compliance
|
||||
# RiskMgmt compliance
|
||||
"max_daily_loss": float(max_daily_loss),
|
||||
"ftmo_compliant": max_daily_loss <= 0.05,
|
||||
"riskmgmt_compliant": max_daily_loss <= 0.05,
|
||||
|
||||
# Signal distribution
|
||||
"signal_long_pct": n_long / total_bars if total_bars > 0 else 0,
|
||||
@@ -1117,7 +1117,7 @@ result = {{
|
||||
"n_short": int((signal_aligned == -1).sum()),
|
||||
"n_neutral": int((signal_aligned == 0).sum()),
|
||||
"max_daily_loss": float(max_daily_loss),
|
||||
"ftmo_compliant": max_daily_loss <= 0.05,
|
||||
"riskmgmt_compliant": max_daily_loss <= 0.05,
|
||||
}}
|
||||
|
||||
def sanitize_val(v):
|
||||
@@ -1595,7 +1595,7 @@ class SmartStrategyGenerator:
|
||||
table.add_column("Trades", justify="right")
|
||||
table.add_column("Max DD", justify="right")
|
||||
table.add_column("Monthly %", justify="right")
|
||||
table.add_column("FTMO", justify="center")
|
||||
table.add_column("RiskMgmt", justify="center")
|
||||
|
||||
for i, s in enumerate(accepted, 1):
|
||||
m = s["metrics"]
|
||||
@@ -1607,7 +1607,7 @@ class SmartStrategyGenerator:
|
||||
str(m.get("n_trades", 0)),
|
||||
f"{m.get('max_drawdown', 0):.1%}",
|
||||
f"{m.get('monthly_return_pct', 0):.2f}%",
|
||||
"✅" if m.get("ftmo_compliant", False) else "❌",
|
||||
"✅" if m.get("riskmgmt_compliant", False) else "❌",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -17,7 +17,7 @@ import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
@@ -58,7 +58,7 @@ def test_frequency(close: pd.Series, factors: list[dict], freq: str, session_fil
|
||||
sig[~is_sess] = 0
|
||||
if sig.abs().sum() < 20: continue
|
||||
|
||||
r = backtest_signal_ftmo(c, sig.fillna(0), txn_cost_bps=TXN_COST_BPS)
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=TXN_COST_BPS)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
if oos_m > 0.5:
|
||||
@@ -90,7 +90,7 @@ def test_combo(close: pd.Series, top_signals: list[dict], freq: str, n: int) ->
|
||||
if not signals: return {}
|
||||
|
||||
combo = pd.DataFrame(signals, index=c.index).fillna(0).mean(axis=1)
|
||||
r = backtest_signal_ftmo(c, combo.fillna(0), txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
r = backtest_signal_risk(c, combo.fillna(0), txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
|
||||
return {
|
||||
"frequency": freq, "n_signals": n,
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Systematic Strategy Generator — kein LLM, nur Mathematik.
|
||||
|
||||
Grid-searched threshold strategies with IC-weighted z-score composites.
|
||||
Optionally trains LightGBM directional classifier.
|
||||
|
||||
Approaches:
|
||||
A) IC-weighted z-score composite (always used as base)
|
||||
B) Grid-search entry/exit thresholds (primary)
|
||||
C) LightGBM directional classifier (optional, if factors ≥ 5)
|
||||
D) Factor-ranking top/bottom deciles (fast baseline)
|
||||
|
||||
Output: Best strategy by OOS Walk-Forward Sharpe, saved to results/strategies_systematic/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
OUT_DIR = Path("results/strategies_systematic")
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
TXN_COST_BPS = 2.14
|
||||
OOS_START = "2024-01-01"
|
||||
WF_WINDOWS = 4
|
||||
|
||||
|
||||
def load_data() -> tuple:
|
||||
"""Load OHLCV close prices and top factors."""
|
||||
ohlcv = pd.read_hdf(DATA_PATH, key="data")
|
||||
close = ohlcv["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.sort_index().dropna()
|
||||
|
||||
factors = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("status") != "success" or d.get("ic") is None:
|
||||
continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_").replace("\\", "_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
if pf.exists():
|
||||
factors.append({"name": name, "ic": d["ic"]})
|
||||
|
||||
factors.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
return close, factors
|
||||
|
||||
|
||||
def load_factor_values(factor_names: list, close: pd.Series) -> pd.DataFrame:
|
||||
"""Load and align factor time series."""
|
||||
data = {}
|
||||
for name in factor_names:
|
||||
safe = name.replace("/", "_").replace("\\", "_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
if not pf.exists():
|
||||
continue
|
||||
series = pd.read_parquet(pf).iloc[:, 0]
|
||||
if isinstance(series.index, pd.MultiIndex):
|
||||
series = series.droplevel(-1)
|
||||
data[name] = series
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
common = close.index.intersection(df.dropna(how="all").index)
|
||||
return df.loc[common].ffill(), close.loc[common]
|
||||
|
||||
|
||||
def compute_ic_weighted_composite(factors_df: pd.DataFrame, ics: dict[str, float]) -> pd.Series:
|
||||
"""Compute z-score normalized, IC-weighted composite signal."""
|
||||
composite = pd.Series(0.0, index=factors_df.index)
|
||||
total_abs_ic = 0.0
|
||||
|
||||
for col in factors_df.columns:
|
||||
if col not in ics:
|
||||
continue
|
||||
ic = ics[col]
|
||||
if abs(ic) < 0.001:
|
||||
continue
|
||||
z = (factors_df[col] - factors_df[col].rolling(20).mean()) / (
|
||||
factors_df[col].rolling(20).std() + 1e-8
|
||||
)
|
||||
weight = ic # Keep sign: if IC < 0, invert factor
|
||||
composite += weight * z
|
||||
total_abs_ic += abs(ic)
|
||||
|
||||
if total_abs_ic > 0:
|
||||
composite /= total_abs_ic
|
||||
return composite
|
||||
|
||||
|
||||
def generate_signal_threshold(composite: pd.Series, entry: float, exit_thresh: float) -> pd.Series:
|
||||
"""Generate signal from composite with entry/exit thresholds (vectorized)."""
|
||||
signal = pd.Series(0, index=composite.index, dtype=float)
|
||||
signal[composite > entry] = 1
|
||||
signal[composite < -entry] = -1
|
||||
# Simple: no hysteresis for speed. Entry = exit.
|
||||
return signal
|
||||
|
||||
|
||||
def generate_signal_ranking(factors_df: pd.DataFrame, ics: dict, top_pct: float = 0.10) -> pd.Series:
|
||||
"""Factor-ranking: top/bottom deciles = long/short, daily rebalanced."""
|
||||
composite = compute_ic_weighted_composite(factors_df, ics)
|
||||
signal = pd.Series(0, index=composite.index)
|
||||
|
||||
for date, group in composite.groupby(composite.index.normalize()):
|
||||
n = len(group)
|
||||
k = max(1, int(n * top_pct))
|
||||
ranked = group.abs().sort_values(ascending=False)
|
||||
top_idx = ranked.index[:k]
|
||||
bot_idx = ranked.index[-k:]
|
||||
signal.loc[top_idx] = np.sign(composite.loc[top_idx])
|
||||
signal.loc[bot_idx] = np.sign(composite.loc[bot_idx]) * -1
|
||||
|
||||
return signal
|
||||
|
||||
|
||||
def grid_search(close: pd.Series, composite: pd.Series, style: str = "swing") -> dict:
|
||||
"""Grid-search optimal entry thresholds."""
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
best = None
|
||||
best_sharpe = -999
|
||||
|
||||
entries = np.arange(0.3, 2.1, 0.3)
|
||||
|
||||
for entry in entries:
|
||||
sig = generate_signal_threshold(composite, entry, 0.0)
|
||||
r = backtest_signal_risk(close, sig, txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
|
||||
wf_sharpe = r.get("wf_oos_sharpe_mean", -999) or -999
|
||||
if wf_sharpe > best_sharpe:
|
||||
best_sharpe = wf_sharpe
|
||||
best = {
|
||||
"entry": entry,
|
||||
"wf_sharpe": wf_sharpe,
|
||||
"oos_sharpe": r.get("oos_sharpe", -999),
|
||||
"oos_monthly": r.get("oos_monthly_return_pct", 0),
|
||||
"oos_dd": r.get("oos_max_drawdown", 0),
|
||||
"oos_trades": r.get("oos_n_trades", 0),
|
||||
"oos_wr": r.get("oos_win_rate", 0),
|
||||
"is_sharpe": r.get("is_sharpe", -999),
|
||||
"consistency": r.get("wf_oos_consistency", 0),
|
||||
"mc_pvalue": r.get("mc_pvalue", 1),
|
||||
"full_result": r,
|
||||
}
|
||||
print(f" entry={entry:.1f} → WF={wf_sharpe:.3f} OOS_S={r.get('oos_sharpe',0):.3f} OOS_M={r.get('oos_monthly_return_pct',0):.2f}%")
|
||||
|
||||
return best
|
||||
|
||||
|
||||
def train_lightgbm(factors_df: pd.DataFrame, close: pd.Series, forward_bars: int = 96) -> Optional[dict]:
|
||||
"""Train LightGBM directional classifier (approach C)."""
|
||||
try:
|
||||
import lightgbm as lgb
|
||||
except ImportError:
|
||||
print(" LightGBM not available — skipping")
|
||||
return None
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
print(" Training LightGBM directional classifier...")
|
||||
fwd_ret = close.pct_change(forward_bars).shift(-forward_bars)
|
||||
common = factors_df.index.intersection(fwd_ret.dropna().index)
|
||||
X = factors_df.loc[common].ffill().values
|
||||
y = np.sign(fwd_ret.loc[common].values)
|
||||
|
||||
split = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split], X[split:]
|
||||
y_train, y_test = y[:split], y[split:]
|
||||
|
||||
model = lgb.LGBMClassifier(n_estimators=200, max_depth=6, num_leaves=31,
|
||||
learning_rate=0.05, random_state=42, verbose=-1)
|
||||
model.fit(X_train, y_train)
|
||||
preds = model.predict(X_test)
|
||||
signal = pd.Series(preds, index=common[split:])
|
||||
|
||||
r = backtest_signal_risk(close.loc[common[split:]], signal,
|
||||
txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
wf = r.get("wf_oos_sharpe_mean", -999) or -999
|
||||
print(f" LightGBM: WF_Sharpe={wf:.3f}")
|
||||
return {
|
||||
"method": "LightGBM",
|
||||
"wf_sharpe": wf,
|
||||
"oos_sharpe": r.get("oos_sharpe", -999),
|
||||
"oos_monthly": r.get("oos_monthly_return_pct", 0),
|
||||
"oos_dd": r.get("oos_max_drawdown", 0),
|
||||
"oos_trades": r.get("oos_n_trades", 0),
|
||||
"full_result": r,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*60}")
|
||||
print(" NexQuant Systematic Strategy Generator")
|
||||
print(f" Cost: {TXN_COST_BPS} bps | OOS: {OOS_START} | WF: {WF_WINDOWS} windows")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
close, factors = load_data()
|
||||
print(f"Loaded: {len(close):,} bars, {len(factors)} factors")
|
||||
|
||||
# Take top-10 diverse factors
|
||||
top_names = [f["name"] for f in factors[:10]]
|
||||
ics = {f["name"]: f["ic"] for f in factors[:10]}
|
||||
factors_df, close_a = load_factor_values(top_names, close)
|
||||
print(f"Aligned: {len(factors_df.columns)} factors, {len(close_a):,} bars\n")
|
||||
|
||||
results = []
|
||||
|
||||
# ---- Approach A+B: IC-weighted z-score + grid-search thresholds ----
|
||||
print("=== A+B: IC-Weighted Z-Score + Grid-Search Thresholds ===")
|
||||
t0 = time.time()
|
||||
composite = compute_ic_weighted_composite(factors_df, ics)
|
||||
best_thresh = grid_search(close_a, composite)
|
||||
if best_thresh:
|
||||
best_thresh["method"] = "IC-weighted + thresholds"
|
||||
best_thresh["composite_style"] = "zscore"
|
||||
best_thresh["factors_used"] = top_names[:5]
|
||||
results.append(best_thresh)
|
||||
print(f" Best: entry={best_thresh['entry']:.1f} exit={best_thresh['exit']:.1f} "
|
||||
f"WF_Sharpe={best_thresh['wf_sharpe']:.3f} ({time.time()-t0:.0f}s)\n")
|
||||
|
||||
# ---- Approach D: Factor-Ranking Top/Bottom ----
|
||||
print("=== D: Factor-Ranking Top/Bottom Deciles ===")
|
||||
t0 = time.time()
|
||||
sig_rank = generate_signal_ranking(factors_df, ics, top_pct=0.10)
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
r_rank = backtest_signal_risk(close_a, sig_rank, txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
wf_rank = r_rank.get("wf_oos_sharpe_mean", -999) or -999
|
||||
results.append({
|
||||
"method": "Factor-Ranking D",
|
||||
"wf_sharpe": wf_rank,
|
||||
"oos_sharpe": r_rank.get("oos_sharpe", -999),
|
||||
"oos_monthly": r_rank.get("oos_monthly_return_pct", 0),
|
||||
"oos_dd": r_rank.get("oos_max_drawdown", 0),
|
||||
"oos_trades": r_rank.get("oos_n_trades", 0),
|
||||
"full_result": r_rank,
|
||||
})
|
||||
print(f" Factor-Ranking: WF_Sharpe={wf_rank:.3f} ({time.time()-t0:.0f}s)\n")
|
||||
|
||||
# ---- Approach C: LightGBM (if enough factors) ----
|
||||
if len(factors_df.columns) >= 5:
|
||||
print("=== C: LightGBM Directional Classifier ===")
|
||||
t0 = time.time()
|
||||
lgb_result = train_lightgbm(factors_df, close_a)
|
||||
if lgb_result:
|
||||
lgb_result["factors_used"] = top_names[:10]
|
||||
results.append(lgb_result)
|
||||
print(f" ({time.time()-t0:.0f}s)\n")
|
||||
|
||||
# ---- Report ----
|
||||
results.sort(key=lambda x: x.get("wf_sharpe", -999) or -999, reverse=True)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" RESULTS (sorted by Walk-Forward OOS Sharpe)")
|
||||
print(f"{'='*60}")
|
||||
print(f"{'Method':<30} {'WF Sharpe':>10} {'OOS Sharpe':>10} {'OOS Mon%':>8} {'OOS DD%':>8}")
|
||||
print("-" * 70)
|
||||
|
||||
for r in results:
|
||||
wf = r.get("wf_sharpe", -999) or -999
|
||||
oos_s = r.get("oos_sharpe", -999)
|
||||
oos_m = (r.get("oos_monthly", 0) or 0)
|
||||
oos_d = (r.get("oos_dd", 0) or 0) * 100
|
||||
print(f"{r['method']:<30} {wf:>10.3f} {oos_s:>10.3f} {oos_m:>8.2f}% {oos_d:>7.1f}%")
|
||||
|
||||
# Save best result
|
||||
if results:
|
||||
best = results[0]
|
||||
best["generated_at"] = datetime.now().isoformat()
|
||||
best["n_factors"] = len(factors_df.columns)
|
||||
best["n_bars"] = len(close_a)
|
||||
best["cost_bps"] = TXN_COST_BPS
|
||||
|
||||
fname = f"systematic_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{best['method'].replace(' ','_')[:40]}.json"
|
||||
with open(OUT_DIR / fname, "w") as f:
|
||||
json.dump({k: v for k, v in best.items() if k != "full_result"}, f, indent=2, default=str)
|
||||
print(f"\nBest strategy saved: {fname}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Unified Loop — fin_quant + autopilot combined.
|
||||
|
||||
Flow:
|
||||
1. fin_quant generates a factor → auto-evaluates
|
||||
2. New factor tested in quick strategy (1h/30min SMA combo)
|
||||
3. Strategy OOS Sharpe feeds back to LLM for better hypotheses
|
||||
4. Factors that produce profitable strategies get priority
|
||||
5. Single process, no wasted LLM calls on dead-end factors
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys, time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
# ── Config ──
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
TXN_COST_BPS = 2.14
|
||||
MIN_MONTHLY_PCT = 0.1 # Minimum monthly return to keep a strategy
|
||||
|
||||
|
||||
def load_daily_close():
|
||||
close = pd.read_hdf(DATA_PATH, key="data")["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
return close.sort_index().dropna()
|
||||
|
||||
|
||||
def test_factor_as_signal(factor_path: Path, close: pd.Series, freq: str = "1h") -> dict | None:
|
||||
"""Quick-test a factor as a trading signal. Returns metrics or None if unprofitable."""
|
||||
try:
|
||||
series = pd.read_parquet(factor_path).iloc[:, 0]
|
||||
if isinstance(series.index, pd.MultiIndex):
|
||||
series = series.droplevel(-1)
|
||||
fac = series.resample(freq).last().reindex(close.index).ffill()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
is_sess = (close.index.hour >= 7) & (close.index.hour < 17)
|
||||
|
||||
best_result = None
|
||||
for direction in [1, -1]:
|
||||
sig = pd.Series(direction * np.sign(fac).fillna(0), index=close.index)
|
||||
sig[~is_sess] = 0
|
||||
if sig.abs().sum() < 20:
|
||||
continue
|
||||
|
||||
r = backtest_signal_risk(close, sig.fillna(0), txn_cost_bps=TXN_COST_BPS)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
|
||||
if oos_m > (best_result["monthly"] if best_result else MIN_MONTHLY_PCT):
|
||||
best_result = {
|
||||
"direction": direction,
|
||||
"monthly": oos_m,
|
||||
"oos_sharpe": r.get("oos_sharpe", -999),
|
||||
"max_dd": r.get("oos_max_drawdown", 0),
|
||||
"trades": r.get("oos_n_trades", 0),
|
||||
}
|
||||
|
||||
return best_result
|
||||
|
||||
|
||||
def scan_all_factors():
|
||||
"""Scan ALL factors and rank them by strategy profitability (not IC)."""
|
||||
close = load_daily_close().resample("1h").last().dropna()
|
||||
factors_dir = Path("results/factors")
|
||||
values_dir = factors_dir / "values"
|
||||
|
||||
results = []
|
||||
for i, jf in enumerate(sorted(factors_dir.glob("*.json"))):
|
||||
try:
|
||||
meta = json.loads(jf.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if meta.get("status") != "success":
|
||||
continue
|
||||
|
||||
name = meta.get("factor_name", jf.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
pf = values_dir / f"{safe}.parquet"
|
||||
if not pf.exists():
|
||||
continue
|
||||
|
||||
bt = test_factor_as_signal(pf, close)
|
||||
if bt:
|
||||
results.append({
|
||||
"factor": name,
|
||||
"ic": meta.get("ic", 0),
|
||||
**bt,
|
||||
})
|
||||
|
||||
if i % 100 == 0:
|
||||
profitable = sum(1 for r in results if r.get("monthly", 0) > 0.5)
|
||||
print(f" Scanned {i}... {profitable} profitable (>0.5%/mon)")
|
||||
|
||||
results.sort(key=lambda x: x.get("monthly", 0), reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*60}")
|
||||
print(" NexQuant Unified Loop — Factor-to-Strategy Pipeline")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print("\n=== PHASE 1: Scan all existing factors as strategies ===\n")
|
||||
t0 = time.time()
|
||||
ranked = scan_all_factors()
|
||||
|
||||
profitable = [r for r in ranked if r.get("monthly", 0) > 0.5]
|
||||
print(f"\n Scanned {len(ranked)} factors in {time.time()-t0:.0f}s")
|
||||
print(f" Profitable (>0.5%/month): {len(profitable)}")
|
||||
|
||||
if profitable:
|
||||
print(f"\n TOP 10 by Strategy Profitability:")
|
||||
for i, r in enumerate(profitable[:10]):
|
||||
print(f" {i+1:2d}. {r['factor'][:45]:45s} Mon={r['monthly']:+.2f}% IC={r['ic']:+.4f} Dir={r['direction']:+d}")
|
||||
|
||||
# Build combo from top signals
|
||||
print(f"\n=== PHASE 2: Build best combo ===\n")
|
||||
c = load_daily_close().resample("1h").last().dropna()
|
||||
is_sess = (c.index.hour >= 7) & (c.index.hour < 17)
|
||||
|
||||
signals = {}
|
||||
for r in profitable[:10]:
|
||||
safe = r["factor"].replace("/", "_")[:150]
|
||||
pf = Path("results/factors/values") / f"{safe}.parquet"
|
||||
try:
|
||||
s = pd.read_parquet(pf).iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex):
|
||||
s = s.droplevel(-1)
|
||||
fac = s.resample("1h").last().reindex(c.index).ffill()
|
||||
sig = pd.Series(r["direction"] * np.sign(fac).fillna(0), index=c.index)
|
||||
sig[~is_sess] = 0
|
||||
signals[r["factor"]] = sig
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
df = pd.DataFrame(signals, index=c.index).fillna(0)
|
||||
cols = list(df.columns)
|
||||
for n in [2, 3, 5, len(cols)]:
|
||||
combo = df[cols[:n]].mean(axis=1)
|
||||
r = backtest_signal_risk(c, combo.fillna(0), txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
|
||||
m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
dd = (r.get("oos_max_drawdown", 0) or 0) * 100
|
||||
t = r.get("oos_n_trades", 0)
|
||||
gap = 10 - m
|
||||
hit = "🎯" if m >= 4 else ""
|
||||
print(f" {n:2d} sig: Mon={m:+.2f}% DD={dd:+.1f}% T={t} Gap2_10%={gap:+.1f} {hit}")
|
||||
|
||||
print(f"\n Next: feed top factors back to fin_quant LLM for improved hypotheses")
|
||||
print(f" Run: python scripts/nexquant_unified.py")
|
||||
return ranked
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,11 +4,11 @@ Realistic backtest of all strategies in results/strategies_new/.
|
||||
Costs modeled per trade:
|
||||
1.5 pip spread + 0.5 pip slippage + 0.35 pip commission = 2.35 pip total
|
||||
|
||||
FTMO 100k rules enforced:
|
||||
RiskMgmt 100k rules enforced:
|
||||
- Max daily loss: 5% of initial balance ($5,000) → no trading rest of day if hit
|
||||
- Max total loss: 10% of initial balance ($10,000) → account blown, simulation ends
|
||||
- Position sizing: 1% equity risk per trade, 10-pip stop (no artificial lot cap)
|
||||
- Max leverage: 1:30 (EU regulation standard, FTMO default)
|
||||
- Max leverage: 1:30 (EU regulation standard, RiskMgmt default)
|
||||
- Compounding: position size grows with equity each trade
|
||||
|
||||
Out-of-sample window: 2024-01-01 onwards (never seen during factor research).
|
||||
@@ -43,9 +43,9 @@ COST_ENTRY = 2.0 * PIP # spread + slippage
|
||||
COST_EXIT = 0.35 * PIP # commission
|
||||
RISK_PCT = 0.015 # 1.5% equity risk per trade
|
||||
STOP = 10 * PIP # 10-pip hard stop
|
||||
MAX_LEVERAGE = 30 # 1:30 max leverage (FTMO / EU standard)
|
||||
FTMO_MAX_DAILY = 0.05 # 5% max daily loss of initial balance
|
||||
FTMO_MAX_TOTAL = 0.10 # 10% max total loss of initial balance
|
||||
MAX_LEVERAGE = 30 # 1:30 max leverage (RiskMgmt / EU standard)
|
||||
RiskMgmt_MAX_DAILY = 0.05 # 5% max daily loss of initial balance
|
||||
RiskMgmt_MAX_TOTAL = 0.10 # 10% max total loss of initial balance
|
||||
OOS_START = "2024-01-01"
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ def _build_signal(factor_names: list[str], full_idx: pd.Index,
|
||||
def _run_engine(sig_arr: np.ndarray, px_arr: np.ndarray,
|
||||
ts_arr: np.ndarray) -> dict:
|
||||
"""
|
||||
FTMO-compliant backtest engine.
|
||||
RiskMgmt-compliant backtest engine.
|
||||
|
||||
Rules enforced:
|
||||
- Daily loss limit: if daily PnL < -5% of initial ($5k), no new trades that day
|
||||
@@ -165,11 +165,11 @@ def _run_engine(sig_arr: np.ndarray, px_arr: np.ndarray,
|
||||
pos = 0
|
||||
|
||||
# Check daily loss limit
|
||||
if (equity - day_start_eq) / INITIAL < -FTMO_MAX_DAILY:
|
||||
if (equity - day_start_eq) / INITIAL < -RiskMgmt_MAX_DAILY:
|
||||
day_blocked = True
|
||||
|
||||
# Check total loss limit → account blown
|
||||
if equity < INITIAL * (1 - FTMO_MAX_TOTAL):
|
||||
if equity < INITIAL * (1 - RiskMgmt_MAX_TOTAL):
|
||||
blown = True
|
||||
break
|
||||
|
||||
@@ -361,22 +361,22 @@ def main() -> None:
|
||||
hits.to_csv(out_hits, index=False)
|
||||
print(f"\nFiltered results saved → {out_hits}")
|
||||
|
||||
# ── FTMO projection for #1 ────────────────────────────────────────────────
|
||||
# ── RiskMgmt projection for #1 ────────────────────────────────────────────────
|
||||
best_row = (hits if not hits.empty else df.sort_values("oos_monthly_pct", ascending=False)).iloc[0]
|
||||
mon = best_row["oos_monthly_pct"]
|
||||
dd = abs(best_row["oos_dd_pct"])
|
||||
gross = 100_000 * mon / 100
|
||||
challenge_m = 10 / max(mon, 0.01)
|
||||
print(f"\n{'='*70}")
|
||||
print(f" FTMO 100k projection — #{1}: {best_row['name']}")
|
||||
print(f" RiskMgmt 100k projection — #{1}: {best_row['name']}")
|
||||
print(f"{'='*70}")
|
||||
print(f" OOS monthly return: {mon:+.2f}%")
|
||||
print(f" Monthly gross profit: ${gross:,.0f}")
|
||||
print(f" Trader share (80%): ${gross*0.8:,.0f} / month")
|
||||
print(f" Trader annual (80%): ${gross*0.8*12:,.0f} / year")
|
||||
print(f" OOS Max Drawdown: {-dd:.2f}% (FTMO limit: 10%)")
|
||||
print(f" OOS Max Drawdown: {-dd:.2f}% (RiskMgmt limit: 10%)")
|
||||
print(f" Challenge duration: ~{challenge_m:.1f} months to hit +10%")
|
||||
print(f" FTMO safe? {'YES ✓' if dd < 8 else 'BORDERLINE ⚠' if dd < 10 else 'NO ✗'}")
|
||||
print(f" RiskMgmt safe? {'YES ✓' if dd < 8 else 'BORDERLINE ⚠' if dd < 10 else 'NO ✗'}")
|
||||
|
||||
|
||||
def _print_table(df: pd.DataFrame) -> None:
|
||||
|
||||
Reference in New Issue
Block a user