Bug Cleanup
This commit is contained in:
+59
-19
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user