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:
@@ -5,18 +5,18 @@ from .risk_management import CorrelationAnalyzer, PortfolioOptimizer, AdvancedRi
|
||||
from .vbt_backtest import (
|
||||
DEFAULT_BARS_PER_YEAR,
|
||||
DEFAULT_TXN_COST_BPS,
|
||||
FTMO_INITIAL_CAPITAL,
|
||||
FTMO_MAX_DAILY_LOSS,
|
||||
FTMO_MAX_TOTAL_LOSS,
|
||||
FTMO_MAX_LEVERAGE,
|
||||
FTMO_RISK_PER_TRADE,
|
||||
INITIAL_CAPITAL,
|
||||
MAX_DAILY_LOSS,
|
||||
MAX_TOTAL_LOSS,
|
||||
MAX_LEVERAGE,
|
||||
RISK_PER_TRADE,
|
||||
OOS_START_DEFAULT,
|
||||
WF_IS_YEARS,
|
||||
WF_OOS_YEARS,
|
||||
WF_STEP_YEARS,
|
||||
backtest_from_forward_returns,
|
||||
backtest_signal,
|
||||
backtest_signal_ftmo,
|
||||
backtest_signal_risk,
|
||||
monte_carlo_trade_pvalue,
|
||||
walk_forward_rolling,
|
||||
)
|
||||
@@ -24,10 +24,10 @@ from .vbt_backtest import (
|
||||
__all__ = [
|
||||
'BacktestMetrics', 'FactorBacktester', 'ResultsDatabase',
|
||||
'CorrelationAnalyzer', 'PortfolioOptimizer', 'AdvancedRiskManager',
|
||||
'backtest_signal', 'backtest_signal_ftmo', 'backtest_from_forward_returns',
|
||||
'backtest_signal', 'backtest_signal_risk', 'backtest_from_forward_returns',
|
||||
'monte_carlo_trade_pvalue', 'walk_forward_rolling',
|
||||
'DEFAULT_BARS_PER_YEAR', 'DEFAULT_TXN_COST_BPS',
|
||||
'FTMO_INITIAL_CAPITAL', 'FTMO_MAX_DAILY_LOSS', 'FTMO_MAX_TOTAL_LOSS',
|
||||
'FTMO_MAX_LEVERAGE', 'FTMO_RISK_PER_TRADE', 'OOS_START_DEFAULT',
|
||||
'INITIAL_CAPITAL', 'MAX_DAILY_LOSS', 'MAX_TOTAL_LOSS',
|
||||
'MAX_LEVERAGE', 'RISK_PER_TRADE', 'OOS_START_DEFAULT',
|
||||
'WF_IS_YEARS', 'WF_OOS_YEARS', 'WF_STEP_YEARS',
|
||||
]
|
||||
|
||||
@@ -38,15 +38,15 @@ DEFAULT_TXN_COST_BPS = 2.14
|
||||
DEFAULT_BARS_PER_YEAR = 252 * 1440 # 252 trading days * 1440 min/day = 362,880
|
||||
EXTREME_BAR_THRESHOLD = 0.05 # |ret| > 5% on a single 1-min bar → suspicious
|
||||
|
||||
# FTMO 100k account rules (enforced in backtest_signal when ftmo=True)
|
||||
FTMO_INITIAL_CAPITAL = 100_000.0
|
||||
FTMO_MAX_DAILY_LOSS = 0.05 # 5% of initial → block new trades rest of day
|
||||
FTMO_MAX_TOTAL_LOSS = 0.10 # 10% of initial → simulation ends
|
||||
# RiskMgmt 100k account rules (enforced in backtest_signal when riskmgmt=True)
|
||||
INITIAL_CAPITAL = 100_000.0
|
||||
MAX_DAILY_LOSS = 0.05 # 5% of initial → block new trades rest of day
|
||||
MAX_TOTAL_LOSS = 0.10 # 10% of initial → simulation ends
|
||||
# Risk-based position sizing: 1.5% equity risk per trade, 10-pip stop, max 1:30 leverage
|
||||
FTMO_RISK_PER_TRADE = 0.015
|
||||
FTMO_STOP_PIPS = 10
|
||||
FTMO_PIP = 0.0001
|
||||
FTMO_MAX_LEVERAGE = 30
|
||||
RISK_PER_TRADE = 0.015
|
||||
STOP_PIPS = 10
|
||||
PIP_SIZE = 0.0001
|
||||
MAX_LEVERAGE = 30
|
||||
|
||||
|
||||
def _compute_trade_pnl(position: pd.Series, strategy_returns: pd.Series) -> pd.Series:
|
||||
@@ -274,31 +274,31 @@ def backtest_signal(
|
||||
return result
|
||||
|
||||
|
||||
def _apply_ftmo_mask(
|
||||
def _apply_risk_mask(
|
||||
signal: pd.Series,
|
||||
close: pd.Series,
|
||||
leverage: float,
|
||||
txn_cost_bps: float,
|
||||
) -> tuple[pd.Series, dict]:
|
||||
"""
|
||||
Apply FTMO daily/total loss rules to a signal series.
|
||||
Apply RiskMgmt daily/total loss rules to a signal series.
|
||||
|
||||
Returns a masked signal (positions zeroed after each limit breach) and
|
||||
a dict of FTMO compliance metrics.
|
||||
a dict of RiskMgmt compliance metrics.
|
||||
"""
|
||||
txn_cost = txn_cost_bps / 10_000.0
|
||||
position = signal.shift(1).fillna(0) * leverage
|
||||
bar_ret = close.pct_change().fillna(0)
|
||||
|
||||
equity = FTMO_INITIAL_CAPITAL
|
||||
peak_day = FTMO_INITIAL_CAPITAL
|
||||
equity = INITIAL_CAPITAL
|
||||
peak_day = INITIAL_CAPITAL
|
||||
masked = signal.copy()
|
||||
|
||||
daily_breaches = 0
|
||||
total_breached = False
|
||||
total_breach_ts: pd.Timestamp | None = None
|
||||
current_day = None
|
||||
day_start_eq = FTMO_INITIAL_CAPITAL
|
||||
day_start_eq = INITIAL_CAPITAL
|
||||
|
||||
pos_prev = 0.0
|
||||
for ts, sig_i in signal.items():
|
||||
@@ -319,24 +319,24 @@ def _apply_ftmo_mask(
|
||||
masked.at[ts] = 0
|
||||
continue
|
||||
|
||||
daily_loss = (equity - day_start_eq) / FTMO_INITIAL_CAPITAL
|
||||
total_loss = (equity - FTMO_INITIAL_CAPITAL) / FTMO_INITIAL_CAPITAL
|
||||
daily_loss = (equity - day_start_eq) / INITIAL_CAPITAL
|
||||
total_loss = (equity - INITIAL_CAPITAL) / INITIAL_CAPITAL
|
||||
|
||||
if daily_loss < -FTMO_MAX_DAILY_LOSS:
|
||||
if daily_loss < -MAX_DAILY_LOSS:
|
||||
daily_breaches += 1
|
||||
day_start_eq = -999 # block rest of day
|
||||
masked.at[ts] = 0
|
||||
|
||||
if total_loss < -FTMO_MAX_TOTAL_LOSS:
|
||||
if total_loss < -MAX_TOTAL_LOSS:
|
||||
total_breached = True
|
||||
total_breach_ts = ts
|
||||
masked.at[ts] = 0
|
||||
|
||||
return masked, {
|
||||
"ftmo_daily_breaches": daily_breaches,
|
||||
"ftmo_total_breached": total_breached,
|
||||
"ftmo_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
|
||||
"ftmo_compliant": not total_breached and daily_breaches == 0,
|
||||
"risk_daily_breaches": daily_breaches,
|
||||
"risk_total_breached": total_breached,
|
||||
"risk_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
|
||||
"risk_compliant": not total_breached and daily_breaches == 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@ def walk_forward_rolling(
|
||||
"""
|
||||
Rolling walk-forward validation: multiple IS/OOS windows shifted by ``step_years``.
|
||||
|
||||
Each window runs an independent FTMO simulation on the IS and OOS slices.
|
||||
Each window runs an independent RiskMgmt simulation on the IS and OOS slices.
|
||||
Produces aggregate OOS statistics to measure cross-time consistency.
|
||||
|
||||
Returns
|
||||
@@ -442,7 +442,7 @@ def walk_forward_rolling(
|
||||
for mask, prefix in [(is_mask, "is"), (oos_mask, "oos")]:
|
||||
close_s = close.loc[mask]
|
||||
signal_s = signal.loc[mask]
|
||||
masked_s, _ = _apply_ftmo_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
masked_s, _ = _apply_risk_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
r = backtest_signal(close=close_s, signal=masked_s,
|
||||
txn_cost_bps=txn_cost_bps, bars_per_year=bars_per_year)
|
||||
window[f"{prefix}_sharpe"] = r.get("sharpe", 0.0)
|
||||
@@ -466,14 +466,14 @@ def walk_forward_rolling(
|
||||
}
|
||||
|
||||
|
||||
def backtest_signal_ftmo(
|
||||
def backtest_signal_risk(
|
||||
close: pd.Series,
|
||||
signal: pd.Series,
|
||||
txn_cost_bps: float = DEFAULT_TXN_COST_BPS,
|
||||
eurusd_price: float = 1.10,
|
||||
risk_pct: float = FTMO_RISK_PER_TRADE,
|
||||
stop_pips: float = FTMO_STOP_PIPS,
|
||||
max_leverage: float = FTMO_MAX_LEVERAGE,
|
||||
risk_pct: float = RISK_PER_TRADE,
|
||||
stop_pips: float = STOP_PIPS,
|
||||
max_leverage: float = MAX_LEVERAGE,
|
||||
bars_per_year: int = DEFAULT_BARS_PER_YEAR,
|
||||
forward_returns: pd.Series | None = None,
|
||||
oos_start: str | None = OOS_START_DEFAULT,
|
||||
@@ -481,15 +481,15 @@ def backtest_signal_ftmo(
|
||||
mc_n_permutations: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
FTMO-compliant backtest of a strategy signal on EUR/USD.
|
||||
RiskMgmt-compliant backtest of a strategy signal on EUR/USD.
|
||||
|
||||
Applies on top of ``backtest_signal``:
|
||||
- Realistic costs: default 2.14 bps (≈ 2.35 pip spread+slippage+commission)
|
||||
- Risk-based position sizing: risk_pct equity per trade, stop_pips hard stop
|
||||
- Max leverage cap: max_leverage (default 1:30, FTMO standard)
|
||||
- FTMO daily loss limit (5%): positions zeroed rest of day after breach
|
||||
- FTMO total loss limit (10%): all positions zeroed after breach
|
||||
- FTMO-specific metrics added to result dict
|
||||
- Max leverage cap: max_leverage (default 1:30, RiskMgmt standard)
|
||||
- RiskMgmt daily loss limit (5%): positions zeroed rest of day after breach
|
||||
- RiskMgmt total loss limit (10%): all positions zeroed after breach
|
||||
- RiskMgmt-specific metrics added to result dict
|
||||
- Walk-forward OOS split: IS metrics (before oos_start) + OOS metrics (after)
|
||||
|
||||
Parameters
|
||||
@@ -507,7 +507,7 @@ def backtest_signal_ftmo(
|
||||
stop_pips : float
|
||||
Hard stop-loss distance in pips (default 10).
|
||||
max_leverage : float
|
||||
Maximum leverage (default 30 = FTMO 1:30).
|
||||
Maximum leverage (default 30 = RiskMgmt 1:30).
|
||||
oos_start : str or None
|
||||
Start of out-of-sample period (ISO date). None disables OOS split.
|
||||
wf_rolling : bool
|
||||
@@ -518,11 +518,11 @@ def backtest_signal_ftmo(
|
||||
When > 0, computes ``mc_pvalue``: fraction of permuted sequences whose
|
||||
total return >= real total return. p < 0.05 indicates a genuine edge.
|
||||
"""
|
||||
stop_price = stop_pips * FTMO_PIP
|
||||
stop_price = stop_pips * PIP_SIZE
|
||||
leverage_by_risk = risk_pct / (stop_price / eurusd_price)
|
||||
leverage = min(leverage_by_risk, max_leverage)
|
||||
|
||||
masked_signal, ftmo_metrics = _apply_ftmo_mask(signal, close, leverage, txn_cost_bps)
|
||||
masked_signal, risk_metrics = _apply_risk_mask(signal, close, leverage, txn_cost_bps)
|
||||
|
||||
result = backtest_signal(
|
||||
close=close,
|
||||
@@ -532,14 +532,14 @@ def backtest_signal_ftmo(
|
||||
forward_returns=forward_returns,
|
||||
)
|
||||
|
||||
result.update(ftmo_metrics)
|
||||
result["ftmo_leverage"] = round(leverage, 2)
|
||||
result["ftmo_risk_pct"] = risk_pct
|
||||
result["ftmo_stop_pips"] = stop_pips
|
||||
result.update(risk_metrics)
|
||||
result["risk_leverage"] = round(leverage, 2)
|
||||
result["risk_risk_pct"] = risk_pct
|
||||
result["risk_stop_pips"] = stop_pips
|
||||
|
||||
# Re-scale reported equity metrics to FTMO_INITIAL_CAPITAL
|
||||
result["ftmo_end_equity"] = FTMO_INITIAL_CAPITAL * (1 + result.get("total_return", 0))
|
||||
result["ftmo_monthly_profit"] = FTMO_INITIAL_CAPITAL * result.get("monthly_return", 0)
|
||||
# Re-scale reported equity metrics to INITIAL_CAPITAL
|
||||
result["risk_end_equity"] = INITIAL_CAPITAL * (1 + result.get("total_return", 0))
|
||||
result["risk_monthly_profit"] = INITIAL_CAPITAL * result.get("monthly_return", 0)
|
||||
|
||||
# Walk-forward OOS split
|
||||
if oos_start is not None:
|
||||
@@ -551,9 +551,9 @@ def backtest_signal_ftmo(
|
||||
if mask.sum() < 100:
|
||||
return
|
||||
close_s = close.loc[mask]
|
||||
signal_s = signal.loc[mask] # raw signal, not masked — fresh FTMO sim per period
|
||||
signal_s = signal.loc[mask] # raw signal, not masked — fresh RiskMgmt sim per period
|
||||
fwd_split = forward_returns.loc[mask] if forward_returns is not None else None
|
||||
masked_s, _ = _apply_ftmo_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
masked_s, _ = _apply_risk_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
split_result = backtest_signal(
|
||||
close=close_s,
|
||||
signal=masked_s,
|
||||
|
||||
@@ -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:
|
||||
|
||||
+192
-192
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ Tests the complete end-to-end pipeline including:
|
||||
- Portfolio Optimization (P7)
|
||||
- Full Pipeline End-to-End
|
||||
- Parallelization
|
||||
- FTMO Compliance
|
||||
- RiskMgmt Compliance
|
||||
|
||||
At least 20 integration tests covering all new features.
|
||||
|
||||
@@ -526,15 +526,15 @@ class TestParallelization:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: FTMO Compliance
|
||||
# Tests: RiskMgmt Compliance
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFTMOCompliance:
|
||||
"""Test FTMO compliance checks for accepted strategies."""
|
||||
class TestRiskMgmtCompliance:
|
||||
"""Test RiskMgmt compliance checks for accepted strategies."""
|
||||
|
||||
def test_stop_loss_compliance(self, mock_strategies, mock_project_structure):
|
||||
"""Test that all strategies have max drawdown within FTMO limits."""
|
||||
"""Test that all strategies have max drawdown within RiskMgmt limits."""
|
||||
strategies_dir = mock_project_structure / "results" / "strategies_new"
|
||||
|
||||
for json_file in strategies_dir.glob("*.json"):
|
||||
@@ -542,7 +542,7 @@ class TestFTMOCompliance:
|
||||
data = json.load(f)
|
||||
|
||||
max_dd = abs(data.get("max_drawdown", 0))
|
||||
# FTMO max drawdown limit: 10%
|
||||
# RiskMgmt max drawdown limit: 10%
|
||||
assert max_dd <= 0.25 or data.get("max_drawdown", 0) < 0
|
||||
|
||||
def test_daily_loss_compliance(self, mock_strategies, mock_project_structure):
|
||||
@@ -554,25 +554,25 @@ class TestFTMOCompliance:
|
||||
data = json.load(f)
|
||||
|
||||
daily_loss = abs(data.get("daily_loss_max", 0))
|
||||
# FTMO daily loss limit: 5%
|
||||
# RiskMgmt daily loss limit: 5%
|
||||
assert daily_loss <= 0.05 or data.get("daily_loss_max", 0) == 0
|
||||
|
||||
def test_portfolio_max_drawdown(self, mock_strategies, portfolio_optimizer):
|
||||
"""Test that optimized portfolio respects FTMO drawdown limits."""
|
||||
"""Test that optimized portfolio respects RiskMgmt drawdown limits."""
|
||||
opt_result = portfolio_optimizer.optimize_portfolio(method="mean_variance")
|
||||
|
||||
if opt_result and "weights" in opt_result:
|
||||
bt_result = portfolio_optimizer.backtest_portfolio(opt_result["weights"])
|
||||
|
||||
if bt_result:
|
||||
# FTMO max drawdown: 10%
|
||||
# RiskMgmt max drawdown: 10%
|
||||
# Portfolio should stay within limits
|
||||
max_dd = abs(bt_result.get("max_drawdown", 0))
|
||||
# Note: This is a soft check as mock data may vary
|
||||
assert max_dd < 0.50 # Generous threshold for mock data
|
||||
|
||||
def test_ftmo_compliance_report(self, mock_strategies, portfolio_optimizer):
|
||||
"""Test generation of FTMO compliance report."""
|
||||
def test_riskmgmt_compliance_report(self, mock_strategies, portfolio_optimizer):
|
||||
"""Test generation of RiskMgmt compliance report."""
|
||||
strategies = portfolio_optimizer._load_strategy_data()
|
||||
|
||||
if not strategies:
|
||||
@@ -922,12 +922,12 @@ class TestSharpeRatioProperties:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 5: FTMO Drawdown Limits
|
||||
# Property 5: RiskMgmt Drawdown Limits
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFTMODrawdownLimits:
|
||||
"""Property: FTMO drawdown invariants."""
|
||||
class TestRiskMgmtDrawdownLimits:
|
||||
"""Property: RiskMgmt drawdown invariants."""
|
||||
|
||||
@given(
|
||||
equity_gain=st.floats(min_value=-0.15, max_value=0.50),
|
||||
@@ -948,17 +948,17 @@ class TestFTMODrawdownLimits:
|
||||
@settings(max_examples=50, deadline=10000)
|
||||
def test_daily_loss_at_5_percent(self, daily_returns):
|
||||
"""Property: daily P&L breach triggers at −5%."""
|
||||
ftmo_daily_max = 0.05
|
||||
riskmgmt_daily_max = 0.05
|
||||
daily_pnl = np.prod(1 + np.array(daily_returns)) - 1
|
||||
breached = daily_pnl < -ftmo_daily_max
|
||||
breached = daily_pnl < -riskmgmt_daily_max
|
||||
assert isinstance(breached, (bool, np.bool_))
|
||||
|
||||
@given(
|
||||
total_return=st.floats(min_value=-0.15, max_value=0.50),
|
||||
)
|
||||
@settings(max_examples=50, deadline=10000)
|
||||
def test_ftmo_end_equity_formula(self, total_return):
|
||||
"""Property: ftmo_end_equity = initial_capital * (1 + total_return)."""
|
||||
def test_riskmgmt_end_equity_formula(self, total_return):
|
||||
"""Property: riskmgmt_end_equity = initial_capital * (1 + total_return)."""
|
||||
initial = 100_000.0
|
||||
end_equity = initial * (1 + total_return)
|
||||
assert end_equity > 0 # Can't go below zero
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestBuildMLModel:
|
||||
result = build_ml_model(factor_data.iloc[:100], close_data.iloc[:100], "swing")
|
||||
assert result is None
|
||||
|
||||
@patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_ftmo")
|
||||
@patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_risk")
|
||||
def test_sufficient_data_returns_dict(self, mock_bt, factor_data, close_data):
|
||||
mock_bt.return_value = {
|
||||
"sharpe": 1.5, "max_drawdown": -0.1, "win_rate": 0.55,
|
||||
@@ -65,7 +65,7 @@ class TestBuildMLModel:
|
||||
assert result["status"] == "accepted"
|
||||
assert result["type"] == "ml_model"
|
||||
|
||||
@patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_ftmo")
|
||||
@patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_risk")
|
||||
def test_negative_oos_rejected(self, mock_bt, factor_data, close_data):
|
||||
mock_bt.return_value = {
|
||||
"sharpe": 1.5, "max_drawdown": -0.1, "win_rate": 0.55,
|
||||
|
||||
@@ -7,7 +7,7 @@ Tests cover:
|
||||
- Parameter space definition and validation
|
||||
- Parameter suggestion mechanisms
|
||||
- Objective function calculation
|
||||
- FTMO penalty logic
|
||||
- RiskMgmt penalty logic
|
||||
- Optuna study creation and configuration
|
||||
- Parameter injection into strategy code
|
||||
- Optimization run (mocked, small trial count)
|
||||
@@ -37,11 +37,11 @@ except ImportError:
|
||||
from rdagent.scenarios.qlib.local.optuna_optimizer import (
|
||||
OptunaOptimizer,
|
||||
PARAMETER_SPACE,
|
||||
FTMO_MAX_STOP_LOSS,
|
||||
FTMO_MAX_DRAWDOWN,
|
||||
FTMO_MAX_DAILY_LOSS,
|
||||
RiskMgmt_MAX_STOP_LOSS,
|
||||
RiskMgmt_MAX_DRAWDOWN,
|
||||
MAX_DAILY_LOSS,
|
||||
PENALTY_MAX_DD,
|
||||
PENALTY_FTMO_VIOLATION,
|
||||
PENALTY_RiskMgmt_VIOLATION,
|
||||
OPTUNA_AVAILABLE,
|
||||
)
|
||||
|
||||
@@ -205,10 +205,10 @@ class TestParameterSpaceDefinition:
|
||||
assert config['choices'] == [5, 10, 15, 20]
|
||||
|
||||
def test_parameter_space_stop_loss_config(self):
|
||||
"""Test stop_loss parameter configuration (FTMO compliant)."""
|
||||
"""Test stop_loss parameter configuration (RiskMgmt compliant)."""
|
||||
config = PARAMETER_SPACE['stop_loss']
|
||||
assert config['type'] == 'categorical'
|
||||
assert all(c <= FTMO_MAX_STOP_LOSS for c in config['choices'])
|
||||
assert all(c <= RiskMgmt_MAX_STOP_LOSS for c in config['choices'])
|
||||
|
||||
def test_parameter_space_take_profit_config(self):
|
||||
"""Test take_profit parameter configuration."""
|
||||
@@ -222,16 +222,16 @@ class TestParameterSpaceDefinition:
|
||||
assert config['type'] == 'categorical'
|
||||
assert config['choices'] == [0.01, 0.015]
|
||||
|
||||
def test_ftmo_constants_correct(self):
|
||||
"""Test FTMO compliance constants."""
|
||||
assert FTMO_MAX_STOP_LOSS == 0.02
|
||||
assert FTMO_MAX_DRAWDOWN == -0.10
|
||||
assert FTMO_MAX_DAILY_LOSS == 0.05
|
||||
def test_riskmgmt_constants_correct(self):
|
||||
"""Test RiskMgmt compliance constants."""
|
||||
assert RiskMgmt_MAX_STOP_LOSS == 0.02
|
||||
assert RiskMgmt_MAX_DRAWDOWN == -0.10
|
||||
assert MAX_DAILY_LOSS == 0.05
|
||||
|
||||
def test_penalty_constants_correct(self):
|
||||
"""Test penalty weight constants."""
|
||||
assert PENALTY_MAX_DD == -10.0
|
||||
assert PENALTY_FTMO_VIOLATION == -50.0
|
||||
assert PENALTY_RiskMgmt_VIOLATION == -50.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -420,15 +420,15 @@ class TestObjectiveFunction:
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FTMO Penalty Tests
|
||||
# RiskMgmt Penalty Tests
|
||||
# =============================================================================
|
||||
|
||||
@pytest.mark.skipif(not OPTUNA_AVAILABLE, reason="Optuna not installed")
|
||||
class TestFTMOPenalties:
|
||||
"""Test FTMO compliance penalties."""
|
||||
class TestRiskMgmtPenalties:
|
||||
"""Test RiskMgmt compliance penalties."""
|
||||
|
||||
def test_penalty_max_drawdown_violation(self, optimizer):
|
||||
"""Test penalty when max drawdown exceeds FTMO limit."""
|
||||
"""Test penalty when max drawdown exceeds RiskMgmt limit."""
|
||||
study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
|
||||
|
||||
with patch.object(optimizer, '_run_backtest_with_params') as mock_bt:
|
||||
@@ -437,7 +437,7 @@ class TestFTMOPenalties:
|
||||
'sharpe_ratio': 1.5,
|
||||
'ic': 0.08,
|
||||
'total_trades': 25,
|
||||
'max_drawdown': -0.12, # Below FTMO_MAX_DRAWDOWN (-0.10)
|
||||
'max_drawdown': -0.12, # Below RiskMgmt_MAX_DRAWDOWN (-0.10)
|
||||
}
|
||||
|
||||
trial = study.ask()
|
||||
@@ -449,10 +449,10 @@ class TestFTMOPenalties:
|
||||
assert history['penalty'] <= PENALTY_MAX_DD
|
||||
|
||||
def test_penalty_stop_loss_violation(self, optimizer):
|
||||
"""Test penalty when stop loss exceeds FTMO maximum."""
|
||||
"""Test penalty when stop loss exceeds RiskMgmt maximum."""
|
||||
study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
|
||||
|
||||
# Create a custom parameter space that allows FTMO-violating values
|
||||
# Create a custom parameter space that allows RiskMgmt-violating values
|
||||
violating_space = {
|
||||
**PARAMETER_SPACE,
|
||||
'stop_loss': {'type': 'categorical', 'choices': [0.01, 0.025, 0.03]},
|
||||
@@ -475,13 +475,13 @@ class TestFTMOPenalties:
|
||||
value = optimizer.objective(trial)
|
||||
|
||||
history = optimizer._optimization_history[-1]
|
||||
assert history['penalty'] <= PENALTY_FTMO_VIOLATION
|
||||
assert history['penalty'] <= PENALTY_RiskMgmt_VIOLATION
|
||||
|
||||
# Restore original space
|
||||
optimizer.parameter_space = optimizer.param_space_original
|
||||
|
||||
def test_no_penalty_compliant_strategy(self, optimizer):
|
||||
"""Test no penalty for FTMO-compliant strategy."""
|
||||
"""Test no penalty for RiskMgmt-compliant strategy."""
|
||||
study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
|
||||
|
||||
with patch.object(optimizer, '_run_backtest_with_params') as mock_bt:
|
||||
@@ -490,7 +490,7 @@ class TestFTMOPenalties:
|
||||
'sharpe_ratio': 1.5,
|
||||
'ic': 0.08,
|
||||
'total_trades': 25,
|
||||
'max_drawdown': -0.05, # Within FTMO limit
|
||||
'max_drawdown': -0.05, # Within RiskMgmt limit
|
||||
}
|
||||
|
||||
trial = study.ask()
|
||||
@@ -517,7 +517,7 @@ class TestFTMOPenalties:
|
||||
'sharpe_ratio': 1.5,
|
||||
'ic': 0.08,
|
||||
'total_trades': 25,
|
||||
'max_drawdown': -0.12, # FTMO violation
|
||||
'max_drawdown': -0.12, # RiskMgmt violation
|
||||
}
|
||||
|
||||
trial = study.ask()
|
||||
@@ -526,7 +526,7 @@ class TestFTMOPenalties:
|
||||
|
||||
history = optimizer._optimization_history[-1]
|
||||
# Both penalties should apply
|
||||
expected_penalty = PENALTY_MAX_DD + PENALTY_FTMO_VIOLATION
|
||||
expected_penalty = PENALTY_MAX_DD + PENALTY_RiskMgmt_VIOLATION
|
||||
assert history['penalty'] == expected_penalty
|
||||
|
||||
|
||||
|
||||
@@ -452,9 +452,9 @@ class TestAcceptanceGate:
|
||||
assert gate.min_sharpe == 0.5
|
||||
assert gate.min_trades == 10
|
||||
assert gate.max_drawdown == -0.15
|
||||
assert gate.ftmo_max_sl == 0.02
|
||||
assert gate.ftmo_max_daily_loss == 0.05
|
||||
assert gate.ftmo_max_dd == 0.10
|
||||
assert gate.riskmgmt_max_sl == 0.02
|
||||
assert gate.riskmgmt_max_daily_loss == 0.05
|
||||
assert gate.riskmgmt_max_dd == 0.10
|
||||
|
||||
def test_evaluate_passing_strategy(self, acceptance_gate):
|
||||
"""Test evaluation of passing strategy."""
|
||||
@@ -474,8 +474,8 @@ class TestAcceptanceGate:
|
||||
assert evaluation['checks']['sharpe']['passed'] is True
|
||||
assert evaluation['checks']['trades']['passed'] is True
|
||||
assert evaluation['checks']['max_drawdown']['passed'] is True
|
||||
assert evaluation['checks']['ftmo_sl']['passed'] is True
|
||||
assert evaluation['checks']['ftmo_max_dd']['passed'] is True
|
||||
assert evaluation['checks']['riskmgmt_sl']['passed'] is True
|
||||
assert evaluation['checks']['riskmgmt_max_dd']['passed'] is True
|
||||
|
||||
def test_evaluate_failing_ic(self, acceptance_gate):
|
||||
"""Test failure due to low IC."""
|
||||
@@ -540,10 +540,10 @@ class TestAcceptanceGate:
|
||||
assert evaluation['passed'] is False
|
||||
assert any('DD' in r or 'drawdown' in r.lower() for r in evaluation['reasons'])
|
||||
assert evaluation['checks']['max_drawdown']['passed'] is False
|
||||
assert evaluation['checks']['ftmo_max_dd']['passed'] is False
|
||||
assert evaluation['checks']['riskmgmt_max_dd']['passed'] is False
|
||||
|
||||
def test_evaluate_failing_ftmo_sl(self, acceptance_gate):
|
||||
"""Test FTMO stop loss violation."""
|
||||
def test_evaluate_failing_riskmgmt_sl(self, acceptance_gate):
|
||||
"""Test RiskMgmt stop loss violation."""
|
||||
result = {
|
||||
'ic': 0.05,
|
||||
'sharpe_ratio': 1.2,
|
||||
@@ -555,7 +555,7 @@ class TestAcceptanceGate:
|
||||
evaluation = acceptance_gate.evaluate(result)
|
||||
|
||||
assert evaluation['passed'] is False
|
||||
assert evaluation['checks']['ftmo_sl']['passed'] is False
|
||||
assert evaluation['checks']['riskmgmt_sl']['passed'] is False
|
||||
|
||||
def test_evaluate_ic_none(self, acceptance_gate):
|
||||
"""Test when IC is None."""
|
||||
|
||||
@@ -193,9 +193,9 @@ class TestRegressionFixedBugs:
|
||||
|
||||
def test_oos_default_enabled(self):
|
||||
"""Feature: OOS/WF is now default."""
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
import inspect
|
||||
source = inspect.signature(backtest_signal_ftmo)
|
||||
source = inspect.signature(backtest_signal_risk)
|
||||
assert source.parameters["wf_rolling"].default is True
|
||||
|
||||
|
||||
@@ -205,15 +205,15 @@ class TestRegressionFixedBugs:
|
||||
|
||||
|
||||
class TestCrossSystemConsistency:
|
||||
def test_backtest_signal_ftmo_consistency(self):
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_ftmo
|
||||
def test_backtest_signal_risk_consistency(self):
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_risk
|
||||
n = 2000
|
||||
dates = pd.date_range("2024-01-01", periods=n, freq="1min")
|
||||
rng = np.random.default_rng(42)
|
||||
close = pd.Series(1.10 * np.exp(np.cumsum(rng.normal(0, 0.0002, n))), index=dates)
|
||||
signal = pd.Series(np.where(rng.normal(0, 1, n) > 0, 1.0, -1.0), index=dates)
|
||||
r1 = backtest_signal(close, signal, txn_cost_bps=2.14)
|
||||
r2 = backtest_signal_ftmo(close, signal, txn_cost_bps=2.14, wf_rolling=False)
|
||||
r2 = backtest_signal_risk(close, signal, txn_cost_bps=2.14, wf_rolling=False)
|
||||
if r1["status"] == "success" and r2.get("status") == "success":
|
||||
assert "sharpe" in r1 and "sharpe" in r2
|
||||
assert -1.0 <= r1["max_drawdown"] <= 0.0
|
||||
|
||||
@@ -66,17 +66,17 @@ class TestLiveTraderMock:
|
||||
def test_script_imports(self):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ftmo_live_trader",
|
||||
PROJECT_ROOT / "git_ignore_folder/live_trading/ftmo_live_trader.py",
|
||||
"riskmgmt_live_trader",
|
||||
PROJECT_ROOT / "git_ignore_folder/live_trading/riskmgmt_live_trader.py",
|
||||
)
|
||||
assert spec is not None
|
||||
|
||||
def test_script_has_required_sections(self):
|
||||
content = (PROJECT_ROOT / "git_ignore_folder/live_trading/ftmo_live_trader.py").read_text()
|
||||
content = (PROJECT_ROOT / "git_ignore_folder/live_trading/riskmgmt_live_trader.py").read_text()
|
||||
assert "RISK_PCT" in content
|
||||
assert "STOP_PIPS" in content
|
||||
assert "TP_PIPS" in content
|
||||
assert "FTMO_DAILY_LIMIT" in content
|
||||
assert "RiskMgmt_DAILY_LIMIT" in content
|
||||
|
||||
|
||||
class TestFactorValuesIntegration:
|
||||
|
||||
@@ -175,22 +175,22 @@ class TestPromptLoader:
|
||||
load_prompt("xyz_nonexistent")
|
||||
|
||||
|
||||
class TestApplyFTMOMask:
|
||||
class TestApplyRiskMgmtMask:
|
||||
def test_output_same_length(self):
|
||||
from rdagent.components.backtesting.vbt_backtest import _apply_ftmo_mask
|
||||
from rdagent.components.backtesting.vbt_backtest import _apply_risk_mask
|
||||
dates = pd.date_range("2024-01-01", periods=100, freq="1min")
|
||||
close = pd.Series(1.10, index=dates)
|
||||
signal = pd.Series(np.where(np.arange(100) % 2 == 0, 1.0, -1.0), index=dates)
|
||||
masked, metrics = _apply_ftmo_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
||||
masked, metrics = _apply_risk_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
||||
assert len(masked) == len(signal)
|
||||
assert isinstance(metrics, dict)
|
||||
|
||||
def test_flat_signal(self):
|
||||
from rdagent.components.backtesting.vbt_backtest import _apply_ftmo_mask
|
||||
from rdagent.components.backtesting.vbt_backtest import _apply_risk_mask
|
||||
dates = pd.date_range("2024-01-01", periods=200, freq="1min")
|
||||
close = pd.Series(1.10, index=dates)
|
||||
signal = pd.Series(0.0, index=dates)
|
||||
masked, metrics = _apply_ftmo_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
||||
masked, metrics = _apply_risk_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
||||
assert isinstance(metrics, dict)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user