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:
TPTBusiness
2026-05-22 15:10:36 +02:00
parent d4611b530e
commit 4758de0eee
29 changed files with 873 additions and 407 deletions
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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 {
+3 -3
View File
@@ -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)
+15 -15
View File
@@ -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()
+2 -2
View File
@@ -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 -9
View File
@@ -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"),
+4 -4
View File
@@ -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(
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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)
+3 -3
View File
@@ -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}%")
+2 -2
View File
@@ -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 -3
View File
@@ -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
+3 -3
View File
@@ -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'],
+6 -6
View File
@@ -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))
+18 -18
View File
@@ -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)
+3 -3
View File
@@ -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,
+300
View File
@@ -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()
+166
View File
@@ -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()
+12 -12
View File
@@ -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: