reorganized data files and enhance backtesting structure, monte carlo sim

This commit is contained in:
moen0
2026-04-13 01:30:34 +02:00
parent 373e589297
commit 7d53f8589a
26 changed files with 65383 additions and 766 deletions
+974 -61
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Can't render this file because it is too large.
File diff suppressed because it is too large Load Diff
+1
View File
@@ -5,6 +5,7 @@ from data.model import Candle
def load_candles(filepath: str) -> list[Candle]:
df = pd.read_csv(filepath, sep=";", header=None, names=["timestamp", "open", "high", "low", "close", "volume"])
df["timestamp"] = pd.to_datetime(df["timestamp"], format="%Y%m%d %H%M%S")
df = df.sort_values("timestamp", kind="mergesort").drop_duplicates(subset=["timestamp"], keep="last")
candles = []
for _, row in df.iterrows():
+14 -2
View File
@@ -1,20 +1,32 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Candle:
time_open: datetime
open: float
high: float
low: float
close: datetime
close: float
volume: float
@dataclass
class Signal:
direction: str
stop_loss: float
entry_price: float
@dataclass
class Trade:
enter_time: datetime
enter_price: float
direction: str
exit_time:datetime
exit_time: datetime
exit_price: float
pnl: float
r_multiple: float = 0.0
partial_tp_taken: bool = False
partial_tp_realized_pnl: float = 0.0
+265 -64
View File
@@ -1,51 +1,222 @@
from data.model import Candle, Trade
from strategies.base import SimpleStrategy
from data.model import Trade
from collections import defaultdict
import math
def run_backtest(
candles: list[Candle],
strategy,
starting_balance: float = 10000.0,
risk_reward: float = 1.0
) -> list[Trade]:
"""
Runs a backtest on a list of candles using the provided strategy.
Returns a list of closed Trades.
"""
def _apply_break_even_if_triggered(position, candle, strategy):
if not position:
return
if not getattr(strategy, "use_break_even", False):
return
if position.get("break_even_armed"):
return
trigger_rr = float(getattr(strategy, "be_trigger_rr", 1.0) or 0.0)
if trigger_rr <= 0:
return
is_long = position["direction"] == "long"
entry = position["entry_price"]
risk_distance = max(position.get("risk_distance", 0.0), 0.0)
if risk_distance <= 0:
return
trigger_price = entry + (risk_distance * trigger_rr) if is_long else entry - (risk_distance * trigger_rr)
reached_trigger = candle.high >= trigger_price if is_long else candle.low <= trigger_price
if reached_trigger:
position["stop_loss"] = entry
position["break_even_armed"] = True
def _apply_partial_tp_if_triggered(position, candle, strategy):
if not position:
return
if not getattr(strategy, "use_partial_tp", False):
return
if position.get("partial_taken"):
return
trigger_rr = float(getattr(strategy, "partial_tp_rr", 1.0) or 0.0)
if trigger_rr <= 0:
return
partial_pct = float(getattr(strategy, "partial_tp_percent", 0.0) or 0.0)
if partial_pct <= 0:
return
close_fraction = min(max(partial_pct / 100.0, 0.0), 1.0)
remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0)
if remaining_fraction <= 0:
position["partial_taken"] = True
return
close_fraction = min(close_fraction, remaining_fraction)
if close_fraction <= 0:
return
is_long = position["direction"] == "long"
entry = position["entry_price"]
risk_distance = max(position.get("risk_distance", 0.0), 0.0)
if risk_distance <= 0:
return
trigger_price = entry + (risk_distance * trigger_rr) if is_long else entry - (risk_distance * trigger_rr)
reached_trigger = candle.high >= trigger_price if is_long else candle.low <= trigger_price
if not reached_trigger:
return
lot_size = max(position.get("lot_size", 0.0), 0.0)
price_move = (trigger_price - entry) if is_long else (entry - trigger_price)
realized_piece = price_move * lot_size * close_fraction
position["realized_pnl"] = position.get("realized_pnl", 0.0) + realized_piece
position["remaining_fraction"] = max(0.0, remaining_fraction - close_fraction)
position["partial_taken"] = True
def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0):
trades = []
position = None
# One-time preparation (e.g. pre-compute indicators)
consecutive_losses = 0
daily_pnl = defaultdict(float)
if hasattr(strategy, "prepare"):
strategy.prepare(candles)
for i, candle in enumerate(candles):
# === 1. Check if we have an open position (SL/TP hit) ===
if position is not None:
hit_sl = False
hit_tp = False
exit_price = None
if position:
_apply_partial_tp_if_triggered(position, candle, strategy)
_apply_break_even_if_triggered(position, candle, strategy)
if position["direction"] == "long":
if candle.low <= position["stop_loss"]:
hit_sl = True
exit_price = position["stop_loss"]
elif candle.high >= position["take_profit"]:
hit_tp = True
exit_price = position["take_profit"]
else: # short
if candle.high >= position["stop_loss"]:
hit_sl = True
exit_price = position["stop_loss"]
elif candle.low <= position["take_profit"]:
hit_tp = True
exit_price = position["take_profit"]
is_long = position["direction"] == "long"
sl, tp = position["stop_loss"], position["take_profit"]
hit_sl = candle.low <= sl if is_long else candle.high >= sl
hit_tp = candle.high >= tp if is_long else candle.low <= tp
if hit_sl or hit_tp:
# Calculate PnL
if position["direction"] == "long":
pnl = exit_price - position["entry_price"]
else: # short
pnl = position["entry_price"] - exit_price
exit_price = sl if hit_sl else tp
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
lot_size = max(position.get("lot_size", 0.0), 0.0)
remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0)
remaining_pnl = price_move * lot_size * remaining_fraction
pnl = position.get("realized_pnl", 0.0) + remaining_pnl
initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12)
r_multiple = pnl / initial_risk
trades.append(Trade(
enter_time=position["enter_time"],
enter_price=position["entry_price"],
direction=position["direction"],
exit_time=candle.time_open,
exit_price=exit_price,
pnl=pnl,
r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_taken", False)),
partial_tp_realized_pnl=float(position.get("realized_pnl", 0.0) or 0.0),
))
position = None
if pnl <= 0:
consecutive_losses += 1
else:
consecutive_losses = 0
daily_pnl[candle.time_open.date()] += pnl
if position is None:
if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses:
continue
if max_daily_loss > 0:
loss_limit = starting_balance * (max_daily_loss / 100)
if daily_pnl[candle.time_open.date()] <= -loss_limit:
continue
signal = strategy.check_signal(candles, i)
if signal is not None:
is_long = signal.direction == "BUY"
entry = signal.entry_price
sl = signal.stop_loss
sl_distance = abs(entry - sl)
if (
sl_distance <= 0
or not math.isfinite(sl_distance)
or not math.isfinite(entry)
or not math.isfinite(sl)
or risk_pct <= 0
):
continue
risk_amount = starting_balance * (risk_pct / 100)
if risk_amount <= 0 or not math.isfinite(risk_amount):
continue
lot_size = risk_amount / sl_distance
if lot_size <= 0 or not math.isfinite(lot_size):
continue
tp = entry + (sl_distance * risk_reward) if is_long else entry - (sl_distance * risk_reward)
position = {
"direction": "long" if is_long else "short",
"entry_price": entry,
"enter_time": candle.time_open,
"stop_loss": sl,
"take_profit": tp,
"risk_distance": sl_distance,
"lot_size": lot_size,
"break_even_armed": False,
"partial_taken": False,
"remaining_fraction": 1.0,
"realized_pnl": 0.0,
"initial_risk_amount": risk_amount,
}
return trades
def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0):
position = None
consecutive_losses = 0
daily_pnl = defaultdict(float)
total = len(candles)
if hasattr(strategy, "prepare"):
strategy.prepare(candles)
yield {"type": "start", "total_candles": total}
progress_interval = max(1, total // 50)
for i, candle in enumerate(candles):
if i % progress_interval == 0:
yield {"type": "progress", "processed_candles": i, "total_candles": total}
if position:
_apply_partial_tp_if_triggered(position, candle, strategy)
_apply_break_even_if_triggered(position, candle, strategy)
is_long = position["direction"] == "long"
sl, tp = position["stop_loss"], position["take_profit"]
hit_sl = candle.low <= sl if is_long else candle.high >= sl
hit_tp = candle.high >= tp if is_long else candle.low <= tp
if hit_sl or hit_tp:
exit_price = sl if hit_sl else tp
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
lot_size = max(position.get("lot_size", 0.0), 0.0)
remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0)
remaining_pnl = price_move * lot_size * remaining_fraction
pnl = position.get("realized_pnl", 0.0) + remaining_pnl
initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12)
r_multiple = pnl / initial_risk
trade = Trade(
enter_time=position["enter_time"],
@@ -53,39 +224,69 @@ def run_backtest(
direction=position["direction"],
exit_time=candle.time_open,
exit_price=exit_price,
pnl=pnl
pnl=pnl,
r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_taken", False)),
partial_tp_realized_pnl=float(position.get("realized_pnl", 0.0) or 0.0),
)
trades.append(trade)
position = None
# === 2. Look for new entry signal only if flat ===
if pnl <= 0:
consecutive_losses += 1
else:
consecutive_losses = 0
daily_pnl[candle.time_open.date()] += pnl
yield {"type": "trade", "trade": trade, "processed_candles": i, "total_candles": total}
if position is None:
signal = strategy.check_signal(candles, i) # Fixed: pass index instead of slicing
if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses:
continue
if max_daily_loss > 0:
loss_limit = starting_balance * (max_daily_loss / 100)
if daily_pnl[candle.time_open.date()] <= -loss_limit:
continue
if signal == "BUY":
atr = candle.high - candle.low
mult = getattr(strategy, "atr_mult", 0.5)
bracket = atr * mult
signal = strategy.check_signal(candles, i)
if signal is not None:
is_long = signal.direction == "BUY"
entry = signal.entry_price
sl = signal.stop_loss
sl_distance = abs(entry - sl)
if (
sl_distance <= 0
or not math.isfinite(sl_distance)
or not math.isfinite(entry)
or not math.isfinite(sl)
or risk_pct <= 0
):
continue
risk_amount = starting_balance * (risk_pct / 100)
if risk_amount <= 0 or not math.isfinite(risk_amount):
continue
lot_size = risk_amount / sl_distance
if lot_size <= 0 or not math.isfinite(lot_size):
continue
tp = entry + (sl_distance * risk_reward) if is_long else entry - (sl_distance * risk_reward)
position = {
"direction": "long",
"entry_price": candle.close,
"direction": "long" if is_long else "short",
"entry_price": entry,
"enter_time": candle.time_open,
"stop_loss": candle.close - bracket,
"take_profit": candle.close + (bracket * risk_reward),
"stop_loss": sl,
"take_profit": tp,
"risk_distance": sl_distance,
"lot_size": lot_size,
"break_even_armed": False,
"partial_taken": False,
"remaining_fraction": 1.0,
"realized_pnl": 0.0,
"initial_risk_amount": risk_amount,
}
elif signal == "SELL":
atr = candle.high - candle.low
mult = getattr(strategy, "atr_mult", 0.5)
bracket = atr * mult
position = {
"direction": "short",
"entry_price": candle.close,
"enter_time": candle.time_open,
"stop_loss": candle.close + bracket,
"take_profit": candle.close - (bracket * risk_reward),
}
return trades
yield {"type": "done", "total_candles": total}
+154
View File
@@ -0,0 +1,154 @@
import random
def _calculate_percentile(data, percentile):
if not data: return 0.0
sorted_data = sorted(data)
index = (len(sorted_data) - 1) * (percentile / 100.0)
lower = int(index)
upper = lower + 1
if upper >= len(sorted_data): return sorted_data[-1]
return sorted_data[lower] + (index - lower) * (sorted_data[upper] - sorted_data[lower])
def _run_metrics(pnls, starting_balance):
if not pnls:
return {"net_pnl": 0.0, "win_rate": 0.0, "profit_factor": 0.0, "max_drawdown_pct": 0.0}
winners = [p for p in pnls if p > 0]
losers = [p for p in pnls if p < 0]
net_pnl = sum(pnls)
trade_count = len(pnls)
win_rate = (len(winners) / trade_count) * 100 if trade_count > 0 else 0.0
gross_profit = sum(winners)
gross_loss = abs(sum(losers))
profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else (999.0 if gross_profit > 0 else 0.0)
equity = starting_balance
peak = starting_balance
max_drawdown_pct = 0.0
for pnl in pnls:
equity += pnl
if equity > peak:
peak = equity
drawdown_pct = ((peak - equity) / peak) * 100 if peak > 0 else 0.0
if drawdown_pct > max_drawdown_pct:
max_drawdown_pct = drawdown_pct
return {
"net_pnl": net_pnl,
"win_rate": win_rate,
"profit_factor": profit_factor,
"max_drawdown_pct": max_drawdown_pct,
}
def run_monte_carlo(
trade_r_multiples,
runs=1000,
starting_balance=10000.0,
risk_per_trade_pct=1.0,
sampling_method="bootstrap",
missed_trade_pct=5.0,
pnl_variation_pct=10.0,
price_noise_pct=0.0,
slippage_per_trade=0.0,
spread_per_trade=0.0,
per_trade_cost=None,
ruin_drawdown_pct=20.0,
seed=None,
):
if not trade_r_multiples:
return {"summary": {"runs": 0}, "distribution": [], "sample_runs": []}
run_count = max(1, int(runs))
risk_pct = max(0.0, float(risk_per_trade_pct)) / 100.0
pnl_var = max(0.0, float(pnl_variation_pct)) / 100.0
price_var = max(0.0, float(price_noise_pct)) / 100.0
miss_pct = max(0.0, float(missed_trade_pct)) / 100.0
ruin_threshold = max(0.0, float(ruin_drawdown_pct))
fixed_cost = float(per_trade_cost) if per_trade_cost is not None else (max(0.0, float(slippage_per_trade)) + max(0.0, float(spread_per_trade)))
effective_var = max(pnl_var, price_var)
rng = random.Random(seed)
run_results = []
base_trades = list(trade_r_multiples)
trade_count = len(base_trades)
for run_idx in range(run_count):
# 1. Generate the Trade Sequence
if sampling_method == "bootstrap":
path = [rng.choice(base_trades) for _ in range(trade_count)]
elif sampling_method == "shuffle":
path = base_trades[:]
rng.shuffle(path)
else:
path = base_trades[:]
adjusted_pnls = []
equity = float(starting_balance)
peak = float(starting_balance)
ruin_hit = False
# 2. Execute the trades sequentially
for base_r in path:
# Execution Risk: Did the broker drop our connection?
if rng.random() < miss_pct:
continue
r_multiple = float(base_r)
# Add volatility noise to the outcome (Slippage)
if effective_var > 0:
r_multiple *= rng.uniform(1 - effective_var, 1 + effective_var)
# Calculate PnL in dollars based on CURRENT equity (Compounding)
pnl = (equity * risk_pct * r_multiple) - fixed_cost
adjusted_pnls.append(pnl)
equity += pnl
# 3. Live Drawdown & Ruin Check (Prevents Zombie Trading)
if equity > peak:
peak = equity
current_dd = ((peak - equity) / peak) * 100 if peak > 0 else 0.0
if current_dd >= ruin_threshold or equity <= 0:
ruin_hit = True
break # Account blown or max DD hit. STOP trading.
# Calculate metrics for the surviving trades
metrics = _run_metrics(adjusted_pnls, starting_balance)
metrics["run"] = run_idx + 1
metrics["ruin"] = ruin_hit
metrics["trades_taken"] = len(adjusted_pnls)
run_results.append(metrics)
# --- Aggregate Statistics ---
pnls = [r["net_pnl"] for r in run_results]
dds = [r["max_drawdown_pct"] for r in run_results]
profitable_runs = sum(1 for p in pnls if p > 0)
ruin_count = sum(1 for r in run_results if r["ruin"])
summary = {
"runs": run_count,
"avg_pnl": round(sum(pnls) / run_count, 2),
"worst_case_pnl_5th_pct": round(_calculate_percentile(pnls, 5), 2), # 95% Confidence you make at least this much
"profitable_run_pct": round((profitable_runs / run_count) * 100, 2),
"avg_max_drawdown_pct": round(sum(dds) / run_count, 2),
"worst_case_dd_95th_pct": round(_calculate_percentile(dds, 95), 2), # 95% Confidence your DD won't exceed this
"worst_max_drawdown_pct": round(max(dds), 2),
"avg_win_rate": round(sum(r["win_rate"] for r in run_results) / run_count, 2),
"avg_profit_factor": round(sum(r["profit_factor"] for r in run_results) / run_count, 2),
"probability_of_ruin": round((ruin_count / run_count) * 100, 2),
}
return {
"summary": summary,
"distribution": run_results,
"sample_runs": run_results,
}
+54 -16
View File
@@ -1,27 +1,65 @@
def find_fvgs(candles):
def find_fvgs(candles, min_gap_size=0.0, impulse_multiplier=0.0):
"""
Find Fair Value Gaps in candle data.
Args:
candles: list of Candle objects
min_gap_size: minimum gap size in price units to filter noise (0 = no filter)
impulse_multiplier: minimum body-to-avg ratio for the middle candle (0 = no filter)
"""
fvgs = []
avg_body = 0
if impulse_multiplier > 0 and len(candles) > 20:
bodies = [abs(c.close - c.open) for c in candles[:20]]
avg_body = sum(bodies) / len(bodies)
for i in range(2, len(candles)):
c1 = candles[i - 2]
c2 = candles[i - 1]
c3 = candles[i]
# Bullish
# Impulse check on middle candle
if impulse_multiplier > 0 and avg_body > 0:
middle_body = abs(c2.close - c2.open)
if middle_body < avg_body * impulse_multiplier:
continue
# Update rolling average
avg_body = (avg_body * 19 + middle_body) / 20
# Bullish FVG
if c1.high < c3.low:
fvgs.append({
"index": i - 1,
"type": "bullish",
"top": c3.low,
"bottom": c1.high
})
gap_size = c3.low - c1.high
if gap_size >= min_gap_size:
fvgs.append({
"index": i - 1,
"type": "bullish",
"top": c3.low,
"bottom": c1.high,
"mitigated": False,
})
# bearish
# Bearish FVG
elif c1.low > c3.high:
fvgs.append({
"index": i - 1,
"type": "bearish",
"top": c1.low,
"bottom": c3.high
})
gap_size = c1.low - c3.high
if gap_size >= min_gap_size:
fvgs.append({
"index": i - 1,
"type": "bearish",
"top": c1.low,
"bottom": c3.high,
"mitigated": False,
})
return fvgs
# Mark mitigated FVGs
for fvg in fvgs:
if fvg["mitigated"]:
continue
if fvg["type"] == "bullish":
if c3.low <= fvg["bottom"]:
fvg["mitigated"] = True
elif fvg["type"] == "bearish":
if c3.high >= fvg["top"]:
fvg["mitigated"] = True
return fvgs
+4 -4
View File
@@ -19,7 +19,7 @@ def find_liquidity_levels(swings, tolerance=0.015, max_distance=100):
"price": avg_price,
"type": "equal_highs",
"count": len(cluster),
"indexes": [s["index"] for s in cluster]
"indexes": [s["index"] for s in cluster],
})
used.add(i)
@@ -30,7 +30,7 @@ def find_liquidity_levels(swings, tolerance=0.015, max_distance=100):
cluster = [l1]
for j, l2 in enumerate(lows):
if j != i and j not in used:
if abs(h1["price"] - h2["price"]) <= tolerance and abs(h1["index"] - h2["index"]) <= max_distance:
if abs(l1["price"] - l2["price"]) <= tolerance and abs(l1["index"] - l2["index"]) <= max_distance:
cluster.append(l2)
used.add(j)
if len(cluster) >= 2:
@@ -39,8 +39,8 @@ def find_liquidity_levels(swings, tolerance=0.015, max_distance=100):
"price": avg_price,
"type": "equal_lows",
"count": len(cluster),
"indexes": [s["index"] for s in cluster]
"indexes": [s["index"] for s in cluster],
})
used.add(i)
return levels
return levels
+27 -16
View File
@@ -1,31 +1,42 @@
def find_order_blocks(candles, structure, min_impulse=0.10):
def find_order_blocks(candles, structure, min_impulse=0.10, min_ob_size=0.0):
"""
Find Order Blocks based on structure breaks.
Args:
candles: list of Candle objects
structure: list of structure points from detect_structure
min_impulse: legacy param (unused, kept for compat)
min_ob_size: minimum OB size in price units (0 = no filter)
"""
obs = []
for point in structure:
if point["label"] == "HH":
# Bullish break of structure, look back for last bearish candle
idx = point["index"]
for j in range(idx - 1, max(idx - 20, 0), -1):
if candles[j].close < candles[j].open:
obs.append({
"index": j,
"type": "bullish",
"top": candles[j].open,
"bottom": candles[j].close
})
size = candles[j].open - candles[j].close
if size >= min_ob_size:
obs.append({
"index": j,
"type": "bullish",
"top": candles[j].open,
"bottom": candles[j].close,
})
break
elif point["label"] == "LL":
# Bearish break of structure, look back for last bullish candle
idx = point["index"]
for j in range(idx - 1, max(idx - 20, 0), -1):
if candles[j].close > candles[j].open:
obs.append({
"index": j,
"type": "bearish",
"top": candles[j].close,
"bottom": candles[j].open
})
size = candles[j].close - candles[j].open
if size >= min_ob_size:
obs.append({
"index": j,
"type": "bearish",
"top": candles[j].close,
"bottom": candles[j].open,
})
break
return obs
return obs
+21 -2
View File
@@ -5,24 +5,43 @@ SESSIONS_EST = {
"london": (time(2, 0), time(5, 0)),
"new_york": (time(7, 0), time(10, 0)),
"london_close": (time(10, 0), time(12, 0)),
"london_ny_overlap": (time(8, 0), time(10, 0)),
}
def in_session(candle_time, session_name):
if session_name == "all":
return True
if session_name not in SESSIONS_EST:
return True
t = candle_time.time()
start, end = SESSIONS_EST[session_name]
if start > end: # crosses midnight
if start > end:
return t >= start or t < end
return start <= t < end
def get_session(candle_time):
for name in SESSIONS_EST:
if name == "all":
continue
if in_session(candle_time, name):
return name
return "off_hours"
def filter_by_session(candles, session_name):
return [c for c in candles if in_session(c.time_open, session_name)]
def in_day_filter(candle_time, allowed_days):
if not allowed_days:
return True
return candle_time.weekday() in allowed_days
def get_asian_range(candles):
asian = filter_by_session(candles, "asian")
if not asian:
@@ -31,4 +50,4 @@ def get_asian_range(candles):
"high": max(c.high for c in asian),
"low": min(c.low for c in asian),
"mid": (max(c.high for c in asian) + min(c.low for c in asian)) / 2,
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ from data.loader import load_candles, resample_candles
from engine.backtester import run_backtest
from strategies.categorical_strategy import CategoricalStrategy
candles_1m = load_candles("data/data.csv")
candles_1m = load_candles("data/gbpjpy_jan.csv")
candles_5m = resample_candles(candles_1m, period=5)
best_pnl = float("-inf")
-4
View File
@@ -45,14 +45,10 @@ for params in tqdm(param_combos, desc="Optimizing ICT Strategy", unit="backtest"
sweep_lookback=params["sweep_lb"],
)
# Accurate timing
t0 = time.perf_counter()
trades = run_backtest(candles_5m, strategy, 10000)
elapsed = time.perf_counter() - t0
# Optional: print every backtest (can be noisy, comment out if you want cleaner output)
# print(f"Backtest took {elapsed:.4f}s | Trades: {len(trades)}")
if len(trades) < 5:
continue
+15 -19
View File
@@ -3,42 +3,38 @@ from engine.backtester import run_backtest
from strategies.ict_strategy import ICTStrategy
import time
# Load data
candles_1m = load_candles("data/data1.csv")
candles_1m = load_candles("data/2023gj.csv")
candles_5m = resample_candles(candles_1m, period=5)
print("Testing different Risk-Reward ratios with optimized ICTStrategy...\n")
# Best params from optimization (you can tweak session/lookback etc. if you want)
strategy = ICTStrategy(
session="new_york", # Best was New York
session="london",
lookback=7,
ob_max_age=20, # Best was 20
ob_max_age=50,
atr_mult=2.5,
use_liquidity_sweep=False, # Best was False
use_liquidity_sweep=True,
sweep_lookback=5,
)
total_start = time.perf_counter()
for rr in [1.0, 1.5, 2.0, 2.5, 3.0]:
t0 = time.perf_counter()
trades = run_backtest(candles_5m, strategy, 10000, risk_reward=rr)
elapsed = time.perf_counter() - t0
if not trades:
print(f"RR={rr}: No trades")
print(f"RR={rr}: No trades ({elapsed:.2f}s)")
continue
total_pnl = sum(t.pnl for t in trades)
winners = [t for t in trades if t.pnl > 0]
losers = [t for t in trades if t.pnl <= 0]
wr = len(winners) / len(trades) * 100 if trades else 0
wr = len(winners) / len(trades) * 100
avg_win = sum(t.pnl for t in winners) / len(winners) if winners else 0
avg_loss = sum(t.pnl for t in losers) / len(losers) if losers else 0
profit_factor = abs(sum(t.pnl for t in winners) / sum(t.pnl for t in losers)) if losers else float('inf')
print(f"RR={rr:4.1f} | Trades={len(trades):4d} | WR={wr:5.1f}% | "
f"PnL={total_pnl:8.2f} | AvgWin={avg_win:6.3f} | AvgLoss={avg_loss:6.3f} | "
f"PF={profit_factor:5.2f} | Time={elapsed:.3f}s")
print(
f"RR={rr}: Trades={len(trades)}, WR={wr:.1f}%, PnL={total_pnl:.2f}, "
f"AvgW={avg_win:.3f}, AvgL={avg_loss:.3f}, Time={elapsed:.2f}s"
)
total_elapsed = time.perf_counter() - total_start
print(f"Total run time: {total_elapsed:.2f}s")