Bug Cleanup

This commit is contained in:
moen0
2026-05-11 19:14:32 +02:00
parent 130da68f4e
commit 473fe3bd88
12 changed files with 348 additions and 150 deletions
+59 -19
View File
@@ -10,7 +10,7 @@ from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from indicators.sessions import set_timezone
BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if BACKEND_DIR not in sys.path: if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR) sys.path.insert(0, BACKEND_DIR)
@@ -141,13 +141,29 @@ def _get_candles_for_timeframe(dataset_id, timeframe):
def _build_strategy( def _build_strategy(
session, lookback, ob_age, atr_mult, use_fvg, use_ob, session="new_york",
proximity_pct, sweep, sweep_lookback, lookback=5,
min_gap_size, impulse_multiplier, require_unmitigated_fvg, ob_age=50,
require_bos_confluence, min_ob_size, require_fvg_ob_confluence, atr_mult=1.5,
asian_sweep_only, day_filter, use_fvg=True,
use_break_even=False, be_trigger_rr=1.0, use_ob=True,
use_partial_tp=False, partial_tp_rr=1.0, partial_tp_percent=50.0, proximity_pct=0.3,
sweep=True,
sweep_lookback=10,
min_gap_size=0.0,
impulse_multiplier=0.0,
require_unmitigated_fvg=True,
require_bos_confluence=False,
min_ob_size=0.0,
require_fvg_ob_confluence=False,
asian_sweep_only=False,
day_filter=None,
use_break_even=False,
be_trigger_rr=1.0,
use_partial_tp=False,
partial_tp_rr=1.0,
partial_tp_percent=50.0,
timezone="est",
): ):
return ICTStrategy( return ICTStrategy(
session=session, session=session,
@@ -172,6 +188,7 @@ def _build_strategy(
use_partial_tp=use_partial_tp, use_partial_tp=use_partial_tp,
partial_tp_rr=partial_tp_rr, partial_tp_rr=partial_tp_rr,
partial_tp_percent=partial_tp_percent, partial_tp_percent=partial_tp_percent,
timezone=timezone,
) )
@@ -189,12 +206,28 @@ def _trade_payload(trade):
} }
def _stats_payload(trades, rr): def _stats_payload(trades, rr, starting_balance=10000.0):
total_pnl = sum(t.pnl for t in trades) total_pnl = sum(t.pnl for t in trades)
winners = [t for t in trades if t.pnl > 0] winners = [t for t in trades if t.pnl > 0]
losers = [t for t in trades if t.pnl <= 0] losers = [t for t in trades if t.pnl <= 0]
partial_tp_trades = [t for t in trades if getattr(t, "partial_tp_taken", False)] partial_tp_trades = [t for t in trades if getattr(t, "partial_tp_taken", False)]
partial_tp_realized_total = sum(float(getattr(t, "partial_tp_realized_pnl", 0.0) or 0.0) for t in partial_tp_trades) partial_tp_realized_total = sum(float(getattr(t, "partial_tp_realized_pnl", 0.0) or 0.0) for t in partial_tp_trades)
pnls = [t.pnl for t in trades]
returns = [(p / starting_balance) for p in pnls] if starting_balance > 0 else []
mean_return = (sum(returns) / len(returns)) if returns else 0.0
variance = (sum((r - mean_return) ** 2 for r in returns) / len(returns)) if returns else 0.0
std_dev = variance ** 0.5
sharpe_ratio = ((mean_return / std_dev) * (len(returns) ** 0.5)) if std_dev > 0 else 0.0
equity_points = _build_equity_points(trades, starting_balance=starting_balance)
peak = equity_points[0] if equity_points else starting_balance
max_drawdown_pct = 0.0
for value in equity_points:
if value > peak:
peak = value
drawdown_pct = ((peak - value) / peak) * 100 if peak > 0 else 0.0
if drawdown_pct > max_drawdown_pct:
max_drawdown_pct = drawdown_pct
return { return {
"total_trades": len(trades), "total_trades": len(trades),
"winners": len(winners), "winners": len(winners),
@@ -208,6 +241,8 @@ def _stats_payload(trades, rr):
"partial_tp_rate": (len(partial_tp_trades) / len(trades) * 100) if trades else 0, "partial_tp_rate": (len(partial_tp_trades) / len(trades) * 100) if trades else 0,
"partial_tp_realized_total": partial_tp_realized_total, "partial_tp_realized_total": partial_tp_realized_total,
"partial_tp_realized_avg": (partial_tp_realized_total / len(partial_tp_trades)) if partial_tp_trades else 0, "partial_tp_realized_avg": (partial_tp_realized_total / len(partial_tp_trades)) if partial_tp_trades else 0,
"sharpe_ratio": round(sharpe_ratio, 6),
"max_drawdown_pct": round(max_drawdown_pct, 6),
} }
@@ -310,7 +345,7 @@ def _risk_metrics(trades, starting_balance=10000.0):
sortino = (mean_pnl / downside_dev) * (trade_count ** 0.5) if downside_dev > 0 else 0.0 sortino = (mean_pnl / downside_dev) * (trade_count ** 0.5) if downside_dev > 0 else 0.0
equity_points = _build_equity_points(trades, starting_balance=starting_balance) equity_points = _build_equity_points(trades, starting_balance=starting_balance)
peak = equity_points[0] peak = equity_points[0] if equity_points else starting_balance
max_drawdown_pct = 0.0 max_drawdown_pct = 0.0
for value in equity_points: for value in equity_points:
if value > peak: if value > peak:
@@ -319,8 +354,9 @@ def _risk_metrics(trades, starting_balance=10000.0):
if drawdown_pct > max_drawdown_pct: if drawdown_pct > max_drawdown_pct:
max_drawdown_pct = drawdown_pct max_drawdown_pct = drawdown_pct
calmar = (net_pnl / max_drawdown_pct) if max_drawdown_pct > 0 else 0.0 calmar = ((net_pnl / starting_balance) * 100 / max_drawdown_pct) if max_drawdown_pct > 0 else 0.0
recovery = (net_pnl / max_drawdown_pct) if max_drawdown_pct > 0 else 0.0 drawdown_amount = starting_balance * (max_drawdown_pct / 100) if max_drawdown_pct > 0 else 0.0
recovery = (net_pnl / drawdown_amount) if drawdown_amount > 0 else 0.0
if trade_count < 80: if trade_count < 80:
trade_score = max(0.0, trade_count / 80) trade_score = max(0.0, trade_count / 80)
@@ -524,10 +560,7 @@ def get_backtest(
max_consecutive_losses: int = 0, max_consecutive_losses: int = 0,
): ):
dataset_id = _resolve_dataset(dataset) dataset_id = _resolve_dataset(dataset)
if "MT5" in dataset.upper(): timezone = "mt5" if "MT5" in dataset.upper() else "est"
set_timezone("mt5")
else:
set_timezone("est")
candles = _get_candles_for_timeframe(dataset_id, timeframe) candles = _get_candles_for_timeframe(dataset_id, timeframe)
strategy = _build_strategy( strategy = _build_strategy(
@@ -541,6 +574,7 @@ def get_backtest(
asian_sweep_only=asian_sweep_only, day_filter=day_filter, asian_sweep_only=asian_sweep_only, day_filter=day_filter,
use_break_even=use_break_even, be_trigger_rr=be_trigger_rr, use_break_even=use_break_even, be_trigger_rr=be_trigger_rr,
use_partial_tp=use_partial_tp, partial_tp_rr=partial_tp_rr, partial_tp_percent=partial_tp_percent, use_partial_tp=use_partial_tp, partial_tp_rr=partial_tp_rr, partial_tp_percent=partial_tp_percent,
timezone=timezone,
) )
trades = run_backtest( trades = run_backtest(
candles, strategy, 10000, risk_reward=rr, candles, strategy, 10000, risk_reward=rr,
@@ -551,7 +585,7 @@ def get_backtest(
return { return {
"trades": [_trade_payload(t) for t in trades], "trades": [_trade_payload(t) for t in trades],
"candle_times": [c.time_open.isoformat() for c in candles], "candle_times": [c.time_open.isoformat() for c in candles],
"stats": _stats_payload(trades, rr), "stats": _stats_payload(trades, rr, starting_balance=10000.0),
} }
@@ -585,6 +619,7 @@ def backtest_monte_carlo(req: MonteCarloRequest):
@app.post("/api/optimize") @app.post("/api/optimize")
def get_optimize(req: OptimizeRequest): def get_optimize(req: OptimizeRequest):
dataset_id = _resolve_dataset(req.dataset) dataset_id = _resolve_dataset(req.dataset)
timezone = "mt5" if "MT5" in dataset_id.upper() else "est"
candles = _get_candles_for_timeframe(dataset_id, req.timeframe) candles = _get_candles_for_timeframe(dataset_id, req.timeframe)
session_list = req.sessions session_list = req.sessions
@@ -709,6 +744,7 @@ def get_optimize(req: OptimizeRequest):
use_partial_tp=params["use_partial_tp"], use_partial_tp=params["use_partial_tp"],
partial_tp_rr=params["partial_tp_rr"], partial_tp_rr=params["partial_tp_rr"],
partial_tp_percent=params["partial_tp_percent"], partial_tp_percent=params["partial_tp_percent"],
timezone=timezone,
) )
trades = run_backtest(candles, strategy, 10000, risk_reward=params["rr"]) trades = run_backtest(candles, strategy, 10000, risk_reward=params["rr"])
@@ -815,6 +851,7 @@ def get_optimize_monte_carlo(
ruin_drawdown_pct: float = Query(default=20.0, ge=0.0, le=100.0), ruin_drawdown_pct: float = Query(default=20.0, ge=0.0, le=100.0),
): ):
dataset_id = _resolve_dataset(dataset) dataset_id = _resolve_dataset(dataset)
timezone = "mt5" if "MT5" in dataset_id.upper() else "est"
candles = _get_candles_for_timeframe(dataset_id, timeframe) candles = _get_candles_for_timeframe(dataset_id, timeframe)
strategy = _build_strategy( strategy = _build_strategy(
@@ -840,6 +877,7 @@ def get_optimize_monte_carlo(
partial_tp_rr=partial_tp_rr, partial_tp_rr=partial_tp_rr,
partial_tp_percent=partial_tp_percent, partial_tp_percent=partial_tp_percent,
day_filter=None, day_filter=None,
timezone=timezone,
) )
trades = run_backtest(candles, strategy, 10000, risk_reward=rr) trades = run_backtest(candles, strategy, 10000, risk_reward=rr)
trade_r_multiples = [getattr(t, "r_multiple", 0.0) for t in trades] trade_r_multiples = [getattr(t, "r_multiple", 0.0) for t in trades]
@@ -948,6 +986,7 @@ def stream_backtest(
max_consecutive_losses: int = 0, max_consecutive_losses: int = 0,
): ):
dataset_id = _resolve_dataset(dataset) dataset_id = _resolve_dataset(dataset)
timezone = "mt5" if "MT5" in dataset_id.upper() else "est"
candles = _get_candles_for_timeframe(dataset_id, timeframe) candles = _get_candles_for_timeframe(dataset_id, timeframe)
strategy = _build_strategy( strategy = _build_strategy(
@@ -961,6 +1000,7 @@ def stream_backtest(
asian_sweep_only=asian_sweep_only, day_filter=day_filter, asian_sweep_only=asian_sweep_only, day_filter=day_filter,
use_break_even=use_break_even, be_trigger_rr=be_trigger_rr, use_break_even=use_break_even, be_trigger_rr=be_trigger_rr,
use_partial_tp=use_partial_tp, partial_tp_rr=partial_tp_rr, partial_tp_percent=partial_tp_percent, use_partial_tp=use_partial_tp, partial_tp_rr=partial_tp_rr, partial_tp_percent=partial_tp_percent,
timezone=timezone,
) )
def _sse(data): def _sse(data):
@@ -993,7 +1033,7 @@ def stream_backtest(
yield _sse({ yield _sse({
"type": "trade", "type": "trade",
"trade": _trade_payload(trade), "trade": _trade_payload(trade),
"stats": _stats_payload(streamed_trades, rr), "stats": _stats_payload(streamed_trades, rr, starting_balance=10000.0),
"processed_candles": event["processed_candles"], "processed_candles": event["processed_candles"],
"total_candles": event["total_candles"], "total_candles": event["total_candles"],
}) })
@@ -1002,7 +1042,7 @@ def stream_backtest(
yield _sse({ yield _sse({
"type": "done", "type": "done",
"trades": [_trade_payload(t) for t in streamed_trades], "trades": [_trade_payload(t) for t in streamed_trades],
"stats": _stats_payload(streamed_trades, rr), "stats": _stats_payload(streamed_trades, rr, starting_balance=10000.0),
"duration_ms": round(duration_ms, 1), "duration_ms": round(duration_ms, 1),
"candle_times": [c.time_open.isoformat() for c in candles], "candle_times": [c.time_open.isoformat() for c in candles],
}) })
-1
View File
@@ -1,4 +1,3 @@
print("File is running")
import pandas as pd import pandas as pd
from data.model import Candle from data.model import Candle
+2
View File
@@ -28,3 +28,5 @@ class Trade:
exit_price: float exit_price: float
pnl: float pnl: float
r_multiple: float = 0.0 r_multiple: float = 0.0
partial_tp_taken: bool = False
partial_tp_realized_pnl: float = 0.0
+121 -4
View File
@@ -30,18 +30,66 @@ def _apply_break_even_if_triggered(position, candle, strategy):
position["break_even_armed"] = True 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_tp_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
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)
if lot_size <= 0:
return
partial_pct = min(partial_pct, 100.0)
partial_lot = lot_size * (partial_pct / 100.0)
if partial_lot <= 0:
return
price_move = (trigger_price - entry) if is_long else (entry - trigger_price)
partial_pnl = price_move * partial_lot
position["lot_size"] = max(lot_size - partial_lot, 0.0)
position["partial_tp_taken"] = True
position["partial_tp_realized_pnl"] = position.get("partial_tp_realized_pnl", 0.0) + partial_pnl
def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0): max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0):
trades = [] trades = []
position = None position = None
consecutive_losses = 0 consecutive_losses = 0
daily_pnl = defaultdict(float) daily_pnl = defaultdict(float)
equity = float(starting_balance)
if hasattr(strategy, "prepare"): if hasattr(strategy, "prepare"):
strategy.prepare(candles) strategy.prepare(candles)
for i, candle in enumerate(candles): for i, candle in enumerate(candles):
if position: if position:
_apply_break_even_if_triggered(position, candle, strategy) _apply_break_even_if_triggered(position, candle, strategy)
_apply_partial_tp_if_triggered(position, candle, strategy)
is_long = position["direction"] == "long" is_long = position["direction"] == "long"
sl, tp = position["stop_loss"], position["take_profit"] sl, tp = position["stop_loss"], position["take_profit"]
@@ -53,7 +101,8 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
exit_price = sl if hit_sl else 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) 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) lot_size = max(position.get("lot_size", 0.0), 0.0)
pnl = price_move * lot_size partial_pnl = float(position.get("partial_tp_realized_pnl", 0.0) or 0.0)
pnl = (price_move * lot_size) + partial_pnl
risk_distance = max(position.get("risk_distance", 0.0), 1e-12) risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
r_multiple = price_move / risk_distance r_multiple = price_move / risk_distance
@@ -65,8 +114,11 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
exit_price=exit_price, exit_price=exit_price,
pnl=pnl, pnl=pnl,
r_multiple=r_multiple, r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_tp_taken", False)),
partial_tp_realized_pnl=partial_pnl,
)) ))
position = None position = None
equity += pnl
if pnl <= 0: if pnl <= 0:
consecutive_losses += 1 consecutive_losses += 1
@@ -76,6 +128,8 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
daily_pnl[candle.time_open.date()] += pnl daily_pnl[candle.time_open.date()] += pnl
if position is None: if position is None:
if equity <= 0:
continue
if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses: if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses:
continue continue
if max_daily_loss > 0: if max_daily_loss > 0:
@@ -99,7 +153,7 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
): ):
continue continue
risk_amount = starting_balance * (risk_pct / 100) risk_amount = equity * (risk_pct / 100)
if risk_amount <= 0 or not math.isfinite(risk_amount): if risk_amount <= 0 or not math.isfinite(risk_amount):
continue continue
@@ -118,8 +172,35 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
"risk_distance": sl_distance, "risk_distance": sl_distance,
"lot_size": lot_size, "lot_size": lot_size,
"break_even_armed": False, "break_even_armed": False,
"partial_tp_taken": False,
"partial_tp_realized_pnl": 0.0,
} }
if position and candles:
last_candle = candles[-1]
is_long = position["direction"] == "long"
exit_price = last_candle.close
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)
partial_pnl = float(position.get("partial_tp_realized_pnl", 0.0) or 0.0)
pnl = (price_move * lot_size) + partial_pnl
risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
r_multiple = price_move / risk_distance
trades.append(Trade(
enter_time=position["enter_time"],
enter_price=position["entry_price"],
direction=position["direction"],
exit_time=last_candle.time_open,
exit_price=exit_price,
pnl=pnl,
r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_tp_taken", False)),
partial_tp_realized_pnl=partial_pnl,
))
equity += pnl
daily_pnl[last_candle.time_open.date()] += pnl
return trades return trades
@@ -129,6 +210,7 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
consecutive_losses = 0 consecutive_losses = 0
daily_pnl = defaultdict(float) daily_pnl = defaultdict(float)
total = len(candles) total = len(candles)
equity = float(starting_balance)
if hasattr(strategy, "prepare"): if hasattr(strategy, "prepare"):
strategy.prepare(candles) strategy.prepare(candles)
@@ -143,6 +225,7 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
if position: if position:
_apply_break_even_if_triggered(position, candle, strategy) _apply_break_even_if_triggered(position, candle, strategy)
_apply_partial_tp_if_triggered(position, candle, strategy)
is_long = position["direction"] == "long" is_long = position["direction"] == "long"
sl, tp = position["stop_loss"], position["take_profit"] sl, tp = position["stop_loss"], position["take_profit"]
@@ -154,7 +237,8 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
exit_price = sl if hit_sl else 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) 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) lot_size = max(position.get("lot_size", 0.0), 0.0)
pnl = price_move * lot_size partial_pnl = float(position.get("partial_tp_realized_pnl", 0.0) or 0.0)
pnl = (price_move * lot_size) + partial_pnl
risk_distance = max(position.get("risk_distance", 0.0), 1e-12) risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
r_multiple = price_move / risk_distance r_multiple = price_move / risk_distance
@@ -166,8 +250,11 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
exit_price=exit_price, exit_price=exit_price,
pnl=pnl, pnl=pnl,
r_multiple=r_multiple, r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_tp_taken", False)),
partial_tp_realized_pnl=partial_pnl,
) )
position = None position = None
equity += pnl
if pnl <= 0: if pnl <= 0:
consecutive_losses += 1 consecutive_losses += 1
@@ -179,6 +266,8 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
yield {"type": "trade", "trade": trade, "processed_candles": i, "total_candles": total} yield {"type": "trade", "trade": trade, "processed_candles": i, "total_candles": total}
if position is None: if position is None:
if equity <= 0:
continue
if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses: if max_consecutive_losses > 0 and consecutive_losses >= max_consecutive_losses:
continue continue
if max_daily_loss > 0: if max_daily_loss > 0:
@@ -202,7 +291,7 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
): ):
continue continue
risk_amount = starting_balance * (risk_pct / 100) risk_amount = equity * (risk_pct / 100)
if risk_amount <= 0 or not math.isfinite(risk_amount): if risk_amount <= 0 or not math.isfinite(risk_amount):
continue continue
@@ -221,6 +310,34 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
"risk_distance": sl_distance, "risk_distance": sl_distance,
"lot_size": lot_size, "lot_size": lot_size,
"break_even_armed": False, "break_even_armed": False,
"partial_tp_taken": False,
"partial_tp_realized_pnl": 0.0,
} }
if position and candles:
last_candle = candles[-1]
is_long = position["direction"] == "long"
exit_price = last_candle.close
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)
partial_pnl = float(position.get("partial_tp_realized_pnl", 0.0) or 0.0)
pnl = (price_move * lot_size) + partial_pnl
risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
r_multiple = price_move / risk_distance
trade = Trade(
enter_time=position["enter_time"],
enter_price=position["entry_price"],
direction=position["direction"],
exit_time=last_candle.time_open,
exit_price=exit_price,
pnl=pnl,
r_multiple=r_multiple,
partial_tp_taken=bool(position.get("partial_tp_taken", False)),
partial_tp_realized_pnl=partial_pnl,
)
equity += pnl
daily_pnl[last_candle.time_open.date()] += pnl
yield {"type": "trade", "trade": trade, "processed_candles": total, "total_candles": total}
yield {"type": "done", "total_candles": total} yield {"type": "done", "total_candles": total}
+21 -14
View File
@@ -22,36 +22,42 @@ SESSIONS_MT5 = {
_active_sessions = SESSIONS_EST _active_sessions = SESSIONS_EST
def get_sessions_for_tz(tz="est"):
if tz and tz.lower() in ("mt5", "utc+2", "server"):
return SESSIONS_MT5
return SESSIONS_EST
def set_timezone(tz="est"): def set_timezone(tz="est"):
global _active_sessions global _active_sessions
if tz.lower() in ("mt5", "utc+2", "server"): _active_sessions = get_sessions_for_tz(tz)
_active_sessions = SESSIONS_MT5
else:
_active_sessions = SESSIONS_EST
def in_session(candle_time, session_name): def in_session(candle_time, session_name, sessions_map=None):
if session_name == "all": if session_name == "all":
return True return True
if session_name not in _active_sessions: active = sessions_map or _active_sessions
if session_name not in active:
return True return True
t = candle_time.time() t = candle_time.time()
start, end = _active_sessions[session_name] start, end = active[session_name]
if start > end: if start > end:
return t >= start or t < end return t >= start or t < end
return start <= t < end return start <= t < end
def get_session(candle_time): def get_session(candle_time, sessions_map=None):
for name in _active_sessions: active = sessions_map or _active_sessions
if in_session(candle_time, name): for name in active:
if in_session(candle_time, name, sessions_map=active):
return name return name
return "off_hours" return "off_hours"
def filter_by_session(candles, session_name): def filter_by_session(candles, session_name, sessions_map=None):
return [c for c in candles if in_session(c.time_open, session_name)] active = sessions_map or _active_sessions
return [c for c in candles if in_session(c.time_open, session_name, sessions_map=active)]
def in_day_filter(candle_time, allowed_days): def in_day_filter(candle_time, allowed_days):
@@ -60,8 +66,9 @@ def in_day_filter(candle_time, allowed_days):
return candle_time.weekday() in allowed_days return candle_time.weekday() in allowed_days
def get_asian_range(candles): def get_asian_range(candles, sessions_map=None):
asian = filter_by_session(candles, "asian") active = sessions_map or _active_sessions
asian = filter_by_session(candles, "asian", sessions_map=active)
if not asian: if not asian:
return None return None
return { return {
+29
View File
@@ -0,0 +1,29 @@
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/gbpjpy_jan.csv")
candles_5m = resample_candles(candles_1m, period=5)
best_pnl = float("-inf")
best_params = None
for lookback in [10, 15, 20, 30, 40, 50]:
for threshold in [0.2, 0.3, 0.4, 0.5, 0.7, 1.0]:
for atr_mult in [0.3, 0.4, 0.5, 0.6, 0.7]:
strategy = CategoricalStrategy(
lookback=lookback,
range_threshold=threshold,
atr_multiplier=atr_mult
)
trades = run_backtest(candles_5m, strategy, 10000)
if len(trades) < 50:
continue
total_pnl = sum(t.pnl for t in trades)
win_rate = len([t for t in trades if t.pnl > 0]) / len(trades) * 100
if total_pnl > best_pnl:
best_pnl = total_pnl
best_params = (lookback, threshold, atr_mult)
print(f"New best: LB={lookback}, TH={threshold}, ATR={atr_mult} -> PnL={total_pnl:.2f}, WR={win_rate:.1f}%, Trades={len(trades)}")
print(f"\nBest: lookback={best_params[0]}, threshold={best_params[1]}, atr_mult={best_params[2]}, PnL={best_pnl:.2f}")
+5 -3
View File
@@ -3,7 +3,7 @@ from indicators.market_structure import find_swing_points, detect_structure
from indicators.liquidity import find_liquidity_levels from indicators.liquidity import find_liquidity_levels
from indicators.fvg import find_fvgs from indicators.fvg import find_fvgs
from indicators.order_blocks import find_order_blocks from indicators.order_blocks import find_order_blocks
from indicators.sessions import in_session, in_day_filter, get_asian_range from indicators.sessions import in_session, in_day_filter, get_asian_range, get_sessions_for_tz
from collections import defaultdict from collections import defaultdict
@@ -33,6 +33,7 @@ class ICTStrategy:
use_partial_tp=False, use_partial_tp=False,
partial_tp_rr=1.0, partial_tp_rr=1.0,
partial_tp_percent=50.0, partial_tp_percent=50.0,
timezone="est",
): ):
self.lookback = lookback self.lookback = lookback
self.atr_mult = atr_mult self.atr_mult = atr_mult
@@ -58,6 +59,7 @@ class ICTStrategy:
self.use_partial_tp = use_partial_tp self.use_partial_tp = use_partial_tp
self.partial_tp_rr = partial_tp_rr self.partial_tp_rr = partial_tp_rr
self.partial_tp_percent = partial_tp_percent self.partial_tp_percent = partial_tp_percent
self.sessions_map = get_sessions_for_tz(timezone)
self.swings = [] self.swings = []
self.structure = [] self.structure = []
@@ -88,7 +90,7 @@ class ICTStrategy:
for c in candles: for c in candles:
daily[c.time_open.date()].append(c) daily[c.time_open.date()].append(c)
for date, day_candles in daily.items(): for date, day_candles in daily.items():
ar = get_asian_range(day_candles) ar = get_asian_range(day_candles, sessions_map=self.sessions_map)
if ar: if ar:
self.asian_ranges[date] = ar self.asian_ranges[date] = ar
@@ -204,7 +206,7 @@ class ICTStrategy:
candle = candles[index] candle = candles[index]
if not in_session(candle.time_open, self.session): if not in_session(candle.time_open, self.session, sessions_map=self.sessions_map):
self.recent_sweep = None self.recent_sweep = None
return None return None
+59 -34
View File
@@ -8,7 +8,7 @@ import {
import { BacktestingTab } from './components/BacktestingTab'; import { BacktestingTab } from './components/BacktestingTab';
import { OptimizerTab } from './components/OptimizerTab'; import { OptimizerTab } from './components/OptimizerTab';
import { TradeHistory } from './components/TradeHistory'; import { TradeHistory } from './components/TradeHistory';
import { motion } from 'motion/react'; import { motion as Motion } from 'motion/react';
import { EquityCurve } from './components/EquityCurve'; import { EquityCurve } from './components/EquityCurve';
import { MetricCard } from './components/MetricCard'; import { MetricCard } from './components/MetricCard';
@@ -115,6 +115,7 @@ export default function App() {
const candleSeriesRef = useRef(null); const candleSeriesRef = useRef(null);
const equitySeriesRef = useRef(null); const equitySeriesRef = useRef(null);
const markersRef = useRef(null); const markersRef = useRef(null);
const abortControllerRef = useRef(null);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [activeTab, setActiveTab] = useState('dashboard'); const [activeTab, setActiveTab] = useState('dashboard');
@@ -194,30 +195,41 @@ export default function App() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
const shouldLoadDashboardData = ['dashboard', 'forex-stats', 'trade-history'].includes(activeTab); const shouldLoadDashboardData = ['dashboard', 'forex-stats', 'trade-history'].includes(activeTab);
if (!shouldLoadDashboardData) { if (!shouldLoadDashboardData) {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
setLoading(false); setLoading(false);
return; return;
} }
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
setLoading(true); setLoading(true);
try { try {
const shouldLoadBacktest = showBacktest || activeTab === 'forex-stats'; const shouldLoadBacktest = showBacktest || activeTab === 'forex-stats';
const shouldFetchBacktest = shouldLoadBacktest && !hasSharedBacktest; const shouldFetchBacktest = shouldLoadBacktest && !hasSharedBacktest;
const datasetQuery = `dataset=${encodeURIComponent(selectedDataset)}`; const datasetQuery = `dataset=${encodeURIComponent(selectedDataset)}`;
const fetches = [ const fetches = [
fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}&${datasetQuery}`), fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}&${datasetQuery}`, { signal: controller.signal }),
fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}&${datasetQuery}`), fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}&${datasetQuery}`, { signal: controller.signal }),
]; ];
if (shouldFetchBacktest) { if (shouldFetchBacktest) {
fetches.push(fetch(`http://localhost:8000/api/backtest?timeframe=${timeframe}&rr=${riskReward}&lookback=${stratParams.lookback}&ob_age=${stratParams.obAge}&atr_mult=${stratParams.atrMult}&sweep=${stratParams.sweep}&sweep_lookback=${stratParams.sweepLookback}&session=${stratParams.session}&${datasetQuery}`)); fetches.push(fetch(`http://localhost:8000/api/backtest?timeframe=${timeframe}&rr=${riskReward}&lookback=${stratParams.lookback}&ob_age=${stratParams.obAge}&atr_mult=${stratParams.atrMult}&sweep=${stratParams.sweep}&sweep_lookback=${stratParams.sweepLookback}&session=${stratParams.session}&${datasetQuery}`, { signal: controller.signal }));
} }
const responses = await Promise.all(fetches); const responses = await Promise.all(fetches);
if (controller.signal.aborted) return;
const candleData = await responses[0].json(); const candleData = await responses[0].json();
const indicatorData = await responses[1].json(); const indicatorData = await responses[1].json();
const backtestPayload = shouldFetchBacktest const backtestPayload = shouldFetchBacktest
? await responses[2].json() ? await responses[2].json()
: (shouldLoadBacktest ? backtestData : null); : (shouldLoadBacktest ? backtestData : null);
if (controller.signal.aborted) return;
const candles = candleData.candles.map((candle) => ({ const candles = candleData.candles.map((candle) => ({
time: Math.floor(new Date(candle.time).getTime() / 1000), time: Math.floor(new Date(candle.time).getTime() / 1000),
open: candle.open, open: candle.open,
@@ -331,10 +343,13 @@ export default function App() {
setBacktestData(backtestPayload); setBacktestData(backtestPayload);
} }
} catch (error) { } catch (error) {
if (error?.name === 'AbortError') return;
console.error('Failed to load data:', error); console.error('Failed to load data:', error);
} finally { } finally {
if (!controller.signal.aborted) {
setLoading(false); setLoading(false);
} }
}
}, [activeTab, backtestData, hasSharedBacktest, indicators, riskReward, selectedDataset, showBacktest, timeframe]); }, [activeTab, backtestData, hasSharedBacktest, indicators, riskReward, selectedDataset, showBacktest, timeframe]);
useEffect(() => { useEffect(() => {
@@ -419,8 +434,18 @@ export default function App() {
const backtestStats = backtestData?.stats ?? null; const backtestStats = backtestData?.stats ?? null;
const equityCurve = useMemo(() => buildEquityCurve(backtestTrades), [backtestTrades]); const equityCurve = useMemo(() => buildEquityCurve(backtestTrades), [backtestTrades]);
const monthlyReturns = useMemo(() => buildMonthlyReturns(backtestTrades), [backtestTrades]); const monthlyReturns = useMemo(() => buildMonthlyReturns(backtestTrades), [backtestTrades]);
const maxDrawdown = useMemo(() => calculateMaxDrawdown(equityCurve), [equityCurve]); const maxDrawdown = useMemo(() => {
const sharpeRatio = useMemo(() => calculateSharpeRatio(backtestTrades), [backtestTrades]); if (backtestStats?.max_drawdown_pct != null) {
return -Math.abs(backtestStats.max_drawdown_pct);
}
return calculateMaxDrawdown(equityCurve);
}, [backtestStats, equityCurve]);
const sharpeRatio = useMemo(() => {
if (backtestStats?.sharpe_ratio != null) {
return backtestStats.sharpe_ratio;
}
return calculateSharpeRatio(backtestTrades);
}, [backtestStats, backtestTrades]);
const largestWin = useMemo(() => backtestTrades.reduce((best, t) => Math.max(best, t.pnl), 0), [backtestTrades]); const largestWin = useMemo(() => backtestTrades.reduce((best, t) => Math.max(best, t.pnl), 0), [backtestTrades]);
const largestLoss = useMemo(() => backtestTrades.reduce((worst, t) => Math.min(worst, t.pnl), 0), [backtestTrades]); const largestLoss = useMemo(() => backtestTrades.reduce((worst, t) => Math.min(worst, t.pnl), 0), [backtestTrades]);
const grossProfit = backtestStats ? backtestStats.winners * backtestStats.avg_win : 0; const grossProfit = backtestStats ? backtestStats.winners * backtestStats.avg_win : 0;
@@ -466,7 +491,7 @@ export default function App() {
<div className="max-w-[1440px] mx-auto px-6 py-8"> <div className="max-w-[1440px] mx-auto px-6 py-8">
{/* Header */} {/* Header */}
<motion.header <Motion.header
className="flex justify-between items-center gap-4 mb-8 flex-wrap" className="flex justify-between items-center gap-4 mb-8 flex-wrap"
variants={itemVariants} variants={itemVariants}
initial="hidden" initial="hidden"
@@ -495,7 +520,7 @@ export default function App() {
<div className="w-2 h-2 bg-[#10b981] animate-pulse" /> <div className="w-2 h-2 bg-[#10b981] animate-pulse" />
)} )}
</div> </div>
</motion.header> </Motion.header>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-2 mb-8 border-b border-[#262626] pb-4"> <div className="flex gap-2 mb-8 border-b border-[#262626] pb-4">
@@ -521,7 +546,7 @@ export default function App() {
</div> </div>
{/* Dashboard Tab */} {/* Dashboard Tab */}
<motion.div <Motion.div
className="space-y-6" className="space-y-6"
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
@@ -529,7 +554,7 @@ export default function App() {
style={{ display: activeTab === 'dashboard' ? 'block' : 'none' }} style={{ display: activeTab === 'dashboard' ? 'block' : 'none' }}
> >
{/* Toolbar */} {/* Toolbar */}
<motion.div variants={itemVariants} className="p-5 border border-[#262626] bg-[#0a0a0a]"> <Motion.div variants={itemVariants} className="p-5 border border-[#262626] bg-[#0a0a0a]">
<div className="flex flex-wrap gap-6 items-center"> <div className="flex flex-wrap gap-6 items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-[11px] text-[#737373] font-mono uppercase tracking-widest">Timeframe</span> <span className="text-[11px] text-[#737373] font-mono uppercase tracking-widest">Timeframe</span>
@@ -606,31 +631,31 @@ export default function App() {
</div> </div>
</div> </div>
</div> </div>
</motion.div> </Motion.div>
{/* Candlestick Chart */} {/* Candlestick Chart */}
<motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Market Chart</p> <p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Market Chart</p>
<h2 className="text-[20px] font-semibold tracking-tight">Candles with structure and trade markers</h2> <h2 className="text-[20px] font-semibold tracking-tight">Candles with structure and trade markers</h2>
</div> </div>
<div ref={chartContainerRef} className="h-[480px] border border-[#1a1a1a] overflow-hidden" /> <div ref={chartContainerRef} className="h-[480px] border border-[#1a1a1a] overflow-hidden" />
</motion.section> </Motion.section>
{/* Equity Line (lightweight-charts) */} {/* Equity Line (lightweight-charts) */}
{showBacktest && ( {showBacktest && (
<motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Equity Curve</p> <p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Equity Curve</p>
<h2 className="text-[20px] font-semibold tracking-tight">Strategy balance progression</h2> <h2 className="text-[20px] font-semibold tracking-tight">Strategy balance progression</h2>
</div> </div>
<div ref={equityChartRef} className="h-[180px] border border-[#1a1a1a] overflow-hidden" /> <div ref={equityChartRef} className="h-[180px] border border-[#1a1a1a] overflow-hidden" />
</motion.section> </Motion.section>
)} )}
</motion.div> </Motion.div>
{/* Stats Tab */} {/* Stats Tab */}
<motion.div <Motion.div
className="space-y-6" className="space-y-6"
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
@@ -638,7 +663,7 @@ export default function App() {
style={{ display: activeTab === 'forex-stats' ? 'block' : 'none' }} style={{ display: activeTab === 'forex-stats' ? 'block' : 'none' }}
> >
{/* Hero */} {/* Hero */}
<motion.section variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-[1.7fr_0.9fr] gap-6 p-8 border border-[#262626] bg-[#0a0a0a]"> <Motion.section variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-[1.7fr_0.9fr] gap-6 p-8 border border-[#262626] bg-[#0a0a0a]">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex gap-3 flex-wrap text-[13px] font-mono"> <div className="flex gap-3 flex-wrap text-[13px] font-mono">
</div> </div>
@@ -656,10 +681,10 @@ export default function App() {
</select> </select>
<p className="text-[11px] text-[#525252] font-mono">Switch CSVs here to refresh all metrics and charts.</p> <p className="text-[11px] text-[#525252] font-mono">Switch CSVs here to refresh all metrics and charts.</p>
</div> </div>
</motion.section> </Motion.section>
{/* Metrics Grid */} {/* Metrics Grid */}
<motion.section variants={itemVariants} className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"> <Motion.section variants={itemVariants} className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{overviewMetrics.map((metric) => ( {overviewMetrics.map((metric) => (
<MetricCard <MetricCard
key={metric.label} key={metric.label}
@@ -671,15 +696,15 @@ export default function App() {
neutral={metric.neutral} neutral={metric.neutral}
/> />
))} ))}
</motion.section> </Motion.section>
{/* Equity Curve (recharts) */} {/* Equity Curve (recharts) */}
<motion.section variants={itemVariants}> <Motion.section variants={itemVariants}>
<EquityCurve data={equityCurve} startingBalance={STARTING_BALANCE} /> <EquityCurve data={equityCurve} startingBalance={STARTING_BALANCE} />
</motion.section> </Motion.section>
{/* Distribution + Breakdown */} {/* Distribution + Breakdown */}
<motion.div variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <Motion.div variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<TradeDistribution <TradeDistribution
wins={backtestStats?.winners ?? 0} wins={backtestStats?.winners ?? 0}
losses={backtestStats?.losers ?? 0} losses={backtestStats?.losers ?? 0}
@@ -695,25 +720,25 @@ export default function App() {
maxDrawdown={maxDrawdown} maxDrawdown={maxDrawdown}
sharpeRatio={sharpeRatio} sharpeRatio={sharpeRatio}
/> />
</motion.div> </Motion.div>
</motion.div> </Motion.div>
{/* Trade History Tab */} {/* Trade History Tab */}
<motion.div <Motion.div
className="space-y-6" className="space-y-6"
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={mounted ? 'visible' : 'hidden'} animate={mounted ? 'visible' : 'hidden'}
style={{ display: activeTab === 'trade-history' ? 'block' : 'none' }} style={{ display: activeTab === 'trade-history' ? 'block' : 'none' }}
> >
<motion.section variants={itemVariants}> <Motion.section variants={itemVariants}>
<TradeHistory trades={backtestTrades} /> <TradeHistory trades={backtestTrades} />
</motion.section> </Motion.section>
</motion.div> </Motion.div>
{/* Backtesting Tab */} {/* Backtesting Tab */}
{activeTab === 'backtesting' && ( {activeTab === 'backtesting' && (
<motion.div <Motion.div
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={mounted ? 'visible' : 'hidden'} animate={mounted ? 'visible' : 'hidden'}
@@ -724,11 +749,11 @@ export default function App() {
onDatasetChange={setSelectedDataset} onDatasetChange={setSelectedDataset}
onBacktestComplete={handleBacktestComplete} onBacktestComplete={handleBacktestComplete}
/> />
</motion.div> </Motion.div>
)} )}
{activeTab === 'optimizer' && ( {activeTab === 'optimizer' && (
<motion.div <Motion.div
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={mounted ? 'visible' : 'hidden'} animate={mounted ? 'visible' : 'hidden'}
@@ -738,7 +763,7 @@ export default function App() {
selectedDataset={selectedDataset} selectedDataset={selectedDataset}
onDatasetChange={setSelectedDataset} onDatasetChange={setSelectedDataset}
/> />
</motion.div> </Motion.div>
)} )}
</div> </div>
</div> </div>
+11 -9
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { motion } from 'motion/react'; import { motion as Motion } from 'motion/react';
import { import {
CandlestickSeries, CandlestickSeries,
LineSeries, LineSeries,
@@ -618,7 +618,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
{/* Header row */} {/* Header row */}
<div className="flex items-center justify-between mb-6 flex-wrap gap-4"> <div className="flex items-center justify-between mb-6 flex-wrap gap-4">
<div> <div>
@@ -817,29 +817,29 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
<ToggleInput label="Use Break-Even" value={useBreakEven} onChange={setUseBreakEven} /> <ToggleInput label="Use Break-Even" value={useBreakEven} onChange={setUseBreakEven} />
<ToggleInput label="Use Partial TP" value={usePartialTp} onChange={setUsePartialTp} /> <ToggleInput label="Use Partial TP" value={usePartialTp} onChange={setUsePartialTp} />
</div> </div>
</motion.div> </Motion.div>
{/* Chart */} {/* Chart */}
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Backtest Chart</p> <p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Backtest Chart</p>
<h2 className="text-[20px] font-semibold tracking-tight">Trade entries and exits</h2> <h2 className="text-[20px] font-semibold tracking-tight">Trade entries and exits</h2>
</div> </div>
<div ref={chartContainerRef} className="h-[420px] border border-[#1a1a1a] overflow-hidden" /> <div ref={chartContainerRef} className="h-[420px] border border-[#1a1a1a] overflow-hidden" />
</motion.div> </Motion.div>
{/* Equity */} {/* Equity */}
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Equity Curve</p> <p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Equity Curve</p>
<h2 className="text-[20px] font-semibold tracking-tight">Balance progression</h2> <h2 className="text-[20px] font-semibold tracking-tight">Balance progression</h2>
</div> </div>
<div ref={equityChartRef} className="h-[160px] border border-[#1a1a1a] overflow-hidden" /> <div ref={equityChartRef} className="h-[160px] border border-[#1a1a1a] overflow-hidden" />
</motion.div> </Motion.div>
{/* Results */} {/* Results */}
{stats && ( {stats && (
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6"> <Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
<div className="mb-6"> <div className="mb-6">
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Results</p> <p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Results</p>
<h2 className="text-[20px] font-semibold tracking-tight">Backtest Summary</h2> <h2 className="text-[20px] font-semibold tracking-tight">Backtest Summary</h2>
@@ -992,8 +992,10 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
<p className={`text-[24px] font-semibold ${partialTpRealized >= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>${formatMoney(partialTpRealized)}</p> <p className={`text-[24px] font-semibold ${partialTpRealized >= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>${formatMoney(partialTpRealized)}</p>
</div> </div>
</div> </div>
</motion.div> </Motion.div>
)} )}
</div> </div>
); );
} }
+12 -36
View File
@@ -1,48 +1,24 @@
import { motion, useInView } from 'motion/react'; import { motion as Motion } from 'motion/react';
import { useEffect, useRef, useState } from 'react'; import { useMemo } from 'react';
export function MetricCard({ label, value, change, isPositive, isPrimary = false, neutral = false }) { export function MetricCard({ label, value, change, isPositive, isPrimary = false, neutral = false }) {
const ref = useRef(null); const displayValue = useMemo(() => {
const isInView = useInView(ref, { once: true, amount: 0.3 });
const [displayValue, setDisplayValue] = useState('0');
useEffect(() => {
if (!isInView) return;
const numericValue = parseFloat(value.replace(/[^0-9.-]/g, '')); const numericValue = parseFloat(value.replace(/[^0-9.-]/g, ''));
if (isNaN(numericValue)) { if (isNaN(numericValue)) return value;
setDisplayValue(value);
return;
}
const duration = 1200;
const startTime = Date.now();
const animate = () => {
const progress = Math.min((Date.now() - startTime) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = numericValue * eased;
if (value.includes('$')) { if (value.includes('$')) {
setDisplayValue(`$${current.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); return `$${numericValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
} else if (value.includes('%')) {
setDisplayValue(`${current.toFixed(1)}%`);
} else {
setDisplayValue(current % 1 === 0 ? Math.round(current).toString() : current.toFixed(2));
} }
if (value.includes('%')) {
if (progress < 1) requestAnimationFrame(animate); return `${numericValue.toFixed(1)}%`;
}; }
return numericValue % 1 === 0 ? Math.round(numericValue).toString() : numericValue.toFixed(2);
animate(); }, [value]);
}, [isInView, value]);
const color = neutral ? 'text-[#fafafa]' : isPositive ? 'text-[#10b981]' : 'text-[#ef4444]'; const color = neutral ? 'text-[#fafafa]' : isPositive ? 'text-[#10b981]' : 'text-[#ef4444]';
const changeLabel = typeof change === 'number' ? `${change >= 0 ? '+' : ''}${change.toFixed(1)}%` : change; const changeLabel = typeof change === 'number' ? `${change >= 0 ? '+' : ''}${change.toFixed(1)}%` : change;
return ( return (
<motion.div <Motion.div
ref={ref}
className={isPrimary ? 'col-span-2 md:col-span-1' : ''} className={isPrimary ? 'col-span-2 md:col-span-1' : ''}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
@@ -76,6 +52,6 @@ export function MetricCard({ label, value, change, isPositive, isPrimary = false
)} )}
</div> </div>
</div> </div>
</motion.div> </Motion.div>
); );
} }
@@ -21,7 +21,7 @@ function CustomTooltip({ active, payload, total }) {
return null; return null;
} }
export function TradeDistribution({ wins = 0, losses = 0, avgWin = 0, avgLoss = 0, largestWin = 0, largestLoss = 0 }) { export function TradeDistribution({ wins = 0, losses = 0, avgWin = 0, avgLoss = 0 }) {
const total = wins + losses; const total = wins + losses;
const data = [ const data = [
{ name: 'Wins', value: wins }, { name: 'Wins', value: wins },
+3 -4
View File
@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { motion } from 'motion/react'; import { motion as Motion } from 'motion/react';
function formatCurrency(value) { function formatCurrency(value) {
const abs = Math.abs(value); const abs = Math.abs(value);
@@ -104,7 +104,6 @@ export function TradeHistory({ trades = [] }) {
pageSlice.map((trade, index) => { pageSlice.map((trade, index) => {
const globalIndex = (safeCurrentPage - 1) * ROWS_PER_PAGE + index; const globalIndex = (safeCurrentPage - 1) * ROWS_PER_PAGE + index;
const isWin = trade.pnl > 0; const isWin = trade.pnl > 0;
const enterDate = new Date(trade.enter_time);
const exitDate = new Date(trade.exit_time); const exitDate = new Date(trade.exit_time);
const dateStr = exitDate.toLocaleDateString('en-CA'); const dateStr = exitDate.toLocaleDateString('en-CA');
const timeStr = exitDate.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeStr = exitDate.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
@@ -112,7 +111,7 @@ export function TradeHistory({ trades = [] }) {
const direction = trade.direction === 'long' ? 'BUY' : 'SELL'; const direction = trade.direction === 'long' ? 'BUY' : 'SELL';
return ( return (
<motion.tr <Motion.tr
key={`${trade.enter_time}-${index}`} key={`${trade.enter_time}-${index}`}
className="border-b border-[#1a1a1a] hover:bg-[#111111] transition-colors" className="border-b border-[#1a1a1a] hover:bg-[#111111] transition-colors"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@@ -142,7 +141,7 @@ export function TradeHistory({ trades = [] }) {
Closed Closed
</span> </span>
</td> </td>
</motion.tr> </Motion.tr>
); );
}) })
)} )}