optimized backtesting effiency
This commit is contained in:
@@ -55,4 +55,53 @@ def get_indicators(timeframe: int = 5):
|
|||||||
"liquidity": levels,
|
"liquidity": levels,
|
||||||
"fvgs": fvgs,
|
"fvgs": fvgs,
|
||||||
"order_blocks": obs
|
"order_blocks": obs
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/backtest")
|
||||||
|
def get_backtest(timeframe: int = 5, rr: float = 2.5):
|
||||||
|
candles_1m = load_candles("data/data.csv")
|
||||||
|
candles = resample_candles(candles_1m, period=timeframe)
|
||||||
|
|
||||||
|
from strategies.ict_strategy import ICTStrategy
|
||||||
|
strategy = ICTStrategy(
|
||||||
|
session="london",
|
||||||
|
lookback=7,
|
||||||
|
ob_max_age=50,
|
||||||
|
atr_mult=2.5,
|
||||||
|
use_liquidity_sweep=True,
|
||||||
|
sweep_lookback=5,
|
||||||
|
)
|
||||||
|
from engine.backtester import run_backtest
|
||||||
|
trades = run_backtest(candles, strategy, 10000, risk_reward=rr)
|
||||||
|
|
||||||
|
candle_times = [c.time_open.isoformat() for c in candles]
|
||||||
|
|
||||||
|
trades_data = []
|
||||||
|
for t in trades:
|
||||||
|
trades_data.append({
|
||||||
|
"enter_time": t.enter_time.isoformat(),
|
||||||
|
"exit_time": t.exit_time.isoformat(),
|
||||||
|
"enter_price": t.enter_price,
|
||||||
|
"exit_price": t.exit_price,
|
||||||
|
"direction": t.direction,
|
||||||
|
"pnl": t.pnl,
|
||||||
|
})
|
||||||
|
|
||||||
|
total_pnl = sum(t.pnl for t in trades)
|
||||||
|
winners = [t for t in trades if t.pnl > 0]
|
||||||
|
losers = [t for t in trades if t.pnl <= 0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trades": trades_data,
|
||||||
|
"candle_times": candle_times,
|
||||||
|
"stats": {
|
||||||
|
"total_trades": len(trades),
|
||||||
|
"winners": len(winners),
|
||||||
|
"losers": len(losers),
|
||||||
|
"win_rate": len(winners) / len(trades) * 100 if trades else 0,
|
||||||
|
"total_pnl": total_pnl,
|
||||||
|
"avg_win": sum(t.pnl for t in winners) / len(winners) if winners else 0,
|
||||||
|
"avg_loss": sum(t.pnl for t in losers) / len(losers) if losers else 0,
|
||||||
|
"risk_reward": rr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+371076
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,91 @@
|
|||||||
from data.model import Candle, Trade
|
from data.model import Candle, Trade
|
||||||
from strategies.base import SimpleStrategy
|
from strategies.base import SimpleStrategy
|
||||||
|
|
||||||
# takes a list of Candles and a starting balance, and returns a list of Trades
|
def run_backtest(
|
||||||
def run_backtest(candles: list[Candle], starting_balance: float) -> list[Trade]:
|
candles: list[Candle],
|
||||||
balance = starting_balance
|
strategy,
|
||||||
|
starting_balance: float = 10000.0,
|
||||||
|
risk_reward: float = 1.0
|
||||||
|
) -> list[Trade]:
|
||||||
|
"""
|
||||||
|
Runs a backtest on a list of candles using the provided strategy.
|
||||||
|
Returns a list of closed Trades.
|
||||||
|
"""
|
||||||
trades = []
|
trades = []
|
||||||
position = None
|
position = None
|
||||||
strategy = SimpleStrategy()
|
|
||||||
|
# One-time preparation (e.g. pre-compute indicators)
|
||||||
|
if hasattr(strategy, "prepare"):
|
||||||
|
strategy.prepare(candles)
|
||||||
|
|
||||||
for i, candle in enumerate(candles):
|
for i, candle in enumerate(candles):
|
||||||
# pass 'i' or the sliced history to the strategy
|
# === 1. Check if we have an open position (SL/TP hit) ===
|
||||||
signal = strategy.check_signal(candles[:i+1])
|
if position is not None:
|
||||||
# If signal and no position, open trade
|
hit_sl = False
|
||||||
if signal == "BUY" and position is None:
|
hit_tp = False
|
||||||
position = {
|
exit_price = None
|
||||||
"type": "long",
|
|
||||||
"entry_price": candle.close,
|
|
||||||
"enter_time": candle.time_open
|
|
||||||
}
|
|
||||||
|
|
||||||
# If signal and in position, close trade
|
if position["direction"] == "long":
|
||||||
elif signal == "SELL" and position is not None:
|
if candle.low <= position["stop_loss"]:
|
||||||
trade = Trade(
|
hit_sl = True
|
||||||
enter_time=position["enter_time"],
|
exit_price = position["stop_loss"]
|
||||||
enter_price=position["entry_price"],
|
elif candle.high >= position["take_profit"]:
|
||||||
direction="long",
|
hit_tp = True
|
||||||
exit_time=candle.time_open,
|
exit_price = position["take_profit"]
|
||||||
exit_price=candle.close,
|
else: # short
|
||||||
pnl=candle.close - position["entry_price"]
|
if candle.high >= position["stop_loss"]:
|
||||||
)
|
hit_sl = True
|
||||||
trades.append(trade)
|
exit_price = position["stop_loss"]
|
||||||
position = None
|
elif candle.low <= position["take_profit"]:
|
||||||
|
hit_tp = True
|
||||||
|
exit_price = position["take_profit"]
|
||||||
|
|
||||||
|
if hit_sl or hit_tp:
|
||||||
|
# Calculate PnL
|
||||||
|
if position["direction"] == "long":
|
||||||
|
pnl = exit_price - position["entry_price"]
|
||||||
|
else: # short
|
||||||
|
pnl = position["entry_price"] - exit_price
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
enter_time=position["enter_time"],
|
||||||
|
enter_price=position["entry_price"],
|
||||||
|
direction=position["direction"],
|
||||||
|
exit_time=candle.time_open,
|
||||||
|
exit_price=exit_price,
|
||||||
|
pnl=pnl
|
||||||
|
)
|
||||||
|
trades.append(trade)
|
||||||
|
position = None
|
||||||
|
|
||||||
|
# === 2. Look for new entry signal only if flat ===
|
||||||
|
if position is None:
|
||||||
|
signal = strategy.check_signal(candles, i) # Fixed: pass index instead of slicing
|
||||||
|
|
||||||
|
if signal == "BUY":
|
||||||
|
atr = candle.high - candle.low
|
||||||
|
mult = getattr(strategy, "atr_mult", 0.5)
|
||||||
|
bracket = atr * mult
|
||||||
|
|
||||||
|
position = {
|
||||||
|
"direction": "long",
|
||||||
|
"entry_price": candle.close,
|
||||||
|
"enter_time": candle.time_open,
|
||||||
|
"stop_loss": candle.close - bracket,
|
||||||
|
"take_profit": candle.close + (bracket * risk_reward),
|
||||||
|
}
|
||||||
|
|
||||||
|
elif signal == "SELL":
|
||||||
|
atr = candle.high - candle.low
|
||||||
|
mult = getattr(strategy, "atr_mult", 0.5)
|
||||||
|
bracket = atr * mult
|
||||||
|
|
||||||
|
position = {
|
||||||
|
"direction": "short",
|
||||||
|
"entry_price": candle.close,
|
||||||
|
"enter_time": candle.time_open,
|
||||||
|
"stop_loss": candle.close + bracket,
|
||||||
|
"take_profit": candle.close - (bracket * risk_reward),
|
||||||
|
}
|
||||||
|
|
||||||
return trades
|
return trades
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from datetime import time
|
||||||
|
|
||||||
|
SESSIONS_EST = {
|
||||||
|
"asian": (time(19, 0), time(3, 0)),
|
||||||
|
"london": (time(2, 0), time(5, 0)),
|
||||||
|
"new_york": (time(7, 0), time(10, 0)),
|
||||||
|
"london_close": (time(10, 0), time(12, 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def in_session(candle_time, session_name):
|
||||||
|
t = candle_time.time()
|
||||||
|
start, end = SESSIONS_EST[session_name]
|
||||||
|
if start > end: # crosses midnight
|
||||||
|
return t >= start or t < end
|
||||||
|
return start <= t < end
|
||||||
|
|
||||||
|
def get_session(candle_time):
|
||||||
|
for name in SESSIONS_EST:
|
||||||
|
if in_session(candle_time, name):
|
||||||
|
return name
|
||||||
|
return "off_hours"
|
||||||
|
|
||||||
|
def filter_by_session(candles, session_name):
|
||||||
|
return [c for c in candles if in_session(c.time_open, session_name)]
|
||||||
|
|
||||||
|
def get_asian_range(candles):
|
||||||
|
asian = filter_by_session(candles, "asian")
|
||||||
|
if not asian:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"high": max(c.high for c in asian),
|
||||||
|
"low": min(c.low for c in asian),
|
||||||
|
"mid": (max(c.high for c in asian) + min(c.low for c in asian)) / 2,
|
||||||
|
}
|
||||||
@@ -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/data.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}")
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from data.loader import load_candles, resample_candles
|
||||||
|
from engine.backtester import run_backtest
|
||||||
|
from strategies.ict_strategy import ICTStrategy
|
||||||
|
import time
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
# Load data once
|
||||||
|
candles_1m = load_candles("data/data1.csv")
|
||||||
|
candles_5m = resample_candles(candles_1m, period=5)
|
||||||
|
|
||||||
|
# Initialize best results
|
||||||
|
best_pnl = float("-inf")
|
||||||
|
best_params = None
|
||||||
|
|
||||||
|
# Generate all parameter combinations
|
||||||
|
param_combos = []
|
||||||
|
for session in ["london", "new_york"]:
|
||||||
|
for lookback in [3, 5, 7, 10]:
|
||||||
|
for ob_age in [20, 50, 80]:
|
||||||
|
for atr in [1.0, 1.5, 2.0, 2.5]:
|
||||||
|
for sweep in [True, False]:
|
||||||
|
sweep_lbs = [5, 10, 15] if sweep else [0]
|
||||||
|
for sweep_lb in sweep_lbs:
|
||||||
|
param_combos.append({
|
||||||
|
"session": session,
|
||||||
|
"lookback": lookback,
|
||||||
|
"ob_age": ob_age,
|
||||||
|
"atr": atr,
|
||||||
|
"sweep": sweep,
|
||||||
|
"sweep_lb": sweep_lb
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"Starting optimization of {len(param_combos)} combinations on your M4 Mac...\n")
|
||||||
|
|
||||||
|
total_start = time.perf_counter()
|
||||||
|
|
||||||
|
# Main loop with progress bar
|
||||||
|
for params in tqdm(param_combos, desc="Optimizing ICT Strategy", unit="backtest"):
|
||||||
|
strategy = ICTStrategy(
|
||||||
|
session=params["session"],
|
||||||
|
lookback=params["lookback"],
|
||||||
|
ob_max_age=params["ob_age"],
|
||||||
|
atr_mult=params["atr"],
|
||||||
|
use_liquidity_sweep=params["sweep"],
|
||||||
|
sweep_lookback=params["sweep_lb"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accurate timing
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
trades = run_backtest(candles_5m, strategy, 10000)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
|
||||||
|
# Optional: print every backtest (can be noisy, comment out if you want cleaner output)
|
||||||
|
# print(f"Backtest took {elapsed:.4f}s | Trades: {len(trades)}")
|
||||||
|
|
||||||
|
if len(trades) < 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
total_pnl = sum(t.pnl for t in trades)
|
||||||
|
wr = len([t for t in trades if t.pnl > 0]) / len(trades) * 100 if trades else 0.0
|
||||||
|
|
||||||
|
if total_pnl > best_pnl:
|
||||||
|
best_pnl = total_pnl
|
||||||
|
best_params = {
|
||||||
|
"session": params["session"],
|
||||||
|
"lookback": params["lookback"],
|
||||||
|
"ob_age": params["ob_age"],
|
||||||
|
"atr": params["atr"],
|
||||||
|
"sweep": params["sweep"],
|
||||||
|
"sweep_lb": params["sweep_lb"],
|
||||||
|
"trades": len(trades),
|
||||||
|
"wr": round(wr, 2)
|
||||||
|
}
|
||||||
|
tqdm.write(f"New best! PnL = {total_pnl:.2f} | Params: {best_params}")
|
||||||
|
|
||||||
|
# Final results
|
||||||
|
total_time = time.perf_counter() - total_start
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("Optimization finished!")
|
||||||
|
print(f"Total time on your M4: {total_time:.1f} seconds ({total_time/60:.1f} minutes)")
|
||||||
|
print(f"Best params: {best_params}")
|
||||||
|
print(f"Best PnL: {best_pnl:.2f}")
|
||||||
|
print("="*60)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from data.loader import load_candles, resample_candles
|
||||||
|
from engine.backtester import run_backtest
|
||||||
|
from strategies.ict_strategy import ICTStrategy
|
||||||
|
import time
|
||||||
|
from tqdm import tqdm
|
||||||
|
from joblib import Parallel, delayed
|
||||||
|
|
||||||
|
print("Loading data...")
|
||||||
|
candles_1m = load_candles("data/data1.csv")
|
||||||
|
candles_5m = resample_candles(candles_1m, period=5)
|
||||||
|
print(f"Loaded {len(candles_5m):,} 5-minute candles.\n")
|
||||||
|
|
||||||
|
param_combos = []
|
||||||
|
for session in ["london", "new_york"]:
|
||||||
|
for lookback in [3, 5, 7, 10]:
|
||||||
|
for ob_age in [20, 50, 80]:
|
||||||
|
for atr in [1.0, 1.5, 2.0, 2.5]:
|
||||||
|
for sweep in [True, False]:
|
||||||
|
sweep_lbs = [5, 10, 15] if sweep else [0]
|
||||||
|
for sweep_lb in sweep_lbs:
|
||||||
|
param_combos.append({
|
||||||
|
"session": session,
|
||||||
|
"lookback": lookback,
|
||||||
|
"ob_age": ob_age,
|
||||||
|
"atr": atr,
|
||||||
|
"sweep": sweep,
|
||||||
|
"sweep_lb": sweep_lb
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"Starting parallel optimization of {len(param_combos)} combinations...\n")
|
||||||
|
|
||||||
|
def run_one_combo(params):
|
||||||
|
strategy = ICTStrategy(
|
||||||
|
session=params["session"],
|
||||||
|
lookback=params["lookback"],
|
||||||
|
ob_max_age=params["ob_age"],
|
||||||
|
atr_mult=params["atr"],
|
||||||
|
use_liquidity_sweep=params["sweep"],
|
||||||
|
sweep_lookback=params["sweep_lb"],
|
||||||
|
)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
trades = run_backtest(candles_5m, strategy, 10000)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if len(trades) < 5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pnl = sum(t.pnl for t in trades)
|
||||||
|
wr = len([t for t in trades if t.pnl > 0]) / len(trades) * 100 if trades else 0.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"params": params,
|
||||||
|
"pnl": total_pnl,
|
||||||
|
"trades": len(trades),
|
||||||
|
"wr": round(wr, 2),
|
||||||
|
"time": round(elapsed, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
total_start = time.perf_counter()
|
||||||
|
|
||||||
|
results = Parallel(n_jobs=-1, verbose=10)(
|
||||||
|
delayed(run_one_combo)(params) for params in param_combos
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_results = [r for r in results if r is not None]
|
||||||
|
|
||||||
|
if not valid_results:
|
||||||
|
print("No valid strategies found with at least 5 trades.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
best_result = max(valid_results, key=lambda x: x["pnl"])
|
||||||
|
|
||||||
|
total_time = time.perf_counter() - total_start
|
||||||
|
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("PARALLEL OPTIMIZATION FINISHED!")
|
||||||
|
print(f"Total time on M4 Mac: {total_time:.1f} seconds ({total_time/60:.1f} minutes)")
|
||||||
|
print(f"Processed {len(param_combos)} combinations at ~{len(param_combos)/total_time:.2f} combos/second")
|
||||||
|
print(f"Best PnL: {best_result['pnl']:.2f}")
|
||||||
|
print(f"Best Params: {best_result['params']}")
|
||||||
|
print(f"Trades: {best_result['trades']} | Win Rate: {best_result['wr']}%")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
print("\nTop 5 results:")
|
||||||
|
for res in sorted(valid_results, key=lambda x: x["pnl"], reverse=True)[:5]:
|
||||||
|
print(f"PnL: {res['pnl']:.2f} | Trades: {res['trades']} | WR: {res['wr']}% | {res['params']}")
|
||||||
+37
-21
@@ -1,28 +1,44 @@
|
|||||||
from data.loader import load_candles, resample_candles
|
from data.loader import load_candles, resample_candles
|
||||||
from indicators.market_structure import find_swing_points, detect_structure
|
from engine.backtester import run_backtest
|
||||||
from indicators.liquidity import find_liquidity_levels
|
from strategies.ict_strategy import ICTStrategy
|
||||||
from indicators.fvg import find_fvgs
|
import time
|
||||||
from indicators.order_blocks import find_order_blocks
|
|
||||||
|
|
||||||
candles_1m = load_candles("data/data.csv")
|
# Load data
|
||||||
candles_3m = resample_candles(candles_1m, period=3)
|
candles_1m = load_candles("data/data1.csv")
|
||||||
candles_5m = resample_candles(candles_1m, period=5)
|
candles_5m = resample_candles(candles_1m, period=5)
|
||||||
|
|
||||||
print(f"1m: {len(candles_1m)} candles")
|
print("Testing different Risk-Reward ratios with optimized ICTStrategy...\n")
|
||||||
print(f"3m: {len(candles_3m)} candles")
|
|
||||||
print(f"5m: {len(candles_5m)} candles")
|
|
||||||
|
|
||||||
swings = find_swing_points(candles_5m)
|
# Best params from optimization (you can tweak session/lookback etc. if you want)
|
||||||
structure = detect_structure(swings)
|
strategy = ICTStrategy(
|
||||||
levels = find_liquidity_levels(swings)
|
session="new_york", # Best was New York
|
||||||
fvgs = find_fvgs(candles_5m)
|
lookback=7,
|
||||||
obs = find_order_blocks(candles_5m, structure)
|
ob_max_age=20, # Best was 20
|
||||||
|
atr_mult=2.5,
|
||||||
|
use_liquidity_sweep=False, # Best was False
|
||||||
|
sweep_lookback=5,
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Swing points: {len(swings)}")
|
for rr in [1.0, 1.5, 2.0, 2.5, 3.0]:
|
||||||
print(f"Structure points: {len(structure)}")
|
t0 = time.perf_counter()
|
||||||
print(f"Liquidity levels: {len(levels)}")
|
|
||||||
print(f"FVGs: {len(fvgs)}")
|
|
||||||
print(f"Order blocks: {len(obs)}")
|
|
||||||
|
|
||||||
for o in obs[:5]:
|
trades = run_backtest(candles_5m, strategy, 10000, risk_reward=rr)
|
||||||
print(o)
|
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if not trades:
|
||||||
|
print(f"RR={rr}: No trades")
|
||||||
|
continue
|
||||||
|
|
||||||
|
total_pnl = sum(t.pnl for t in trades)
|
||||||
|
winners = [t for t in trades if t.pnl > 0]
|
||||||
|
losers = [t for t in trades if t.pnl <= 0]
|
||||||
|
|
||||||
|
wr = len(winners) / len(trades) * 100 if trades else 0
|
||||||
|
avg_win = sum(t.pnl for t in winners) / len(winners) if winners else 0
|
||||||
|
avg_loss = sum(t.pnl for t in losers) / len(losers) if losers else 0
|
||||||
|
profit_factor = abs(sum(t.pnl for t in winners) / sum(t.pnl for t in losers)) if losers else float('inf')
|
||||||
|
|
||||||
|
print(f"RR={rr:4.1f} | Trades={len(trades):4d} | WR={wr:5.1f}% | "
|
||||||
|
f"PnL={total_pnl:8.2f} | AvgWin={avg_win:6.3f} | AvgLoss={avg_loss:6.3f} | "
|
||||||
|
f"PF={profit_factor:5.2f} | Time={elapsed:.3f}s")
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
class CategoricalStrategy:
|
||||||
|
def __init__(self, lookback=20, range_threshold=0.4, atr_multiplier=0.5):
|
||||||
|
self.lookback = lookback
|
||||||
|
self.range_threshold = range_threshold
|
||||||
|
self.atr_multiplier = atr_multiplier
|
||||||
|
|
||||||
|
def get_atr1(self, candle):
|
||||||
|
return candle.high - candle.low
|
||||||
|
|
||||||
|
def classify(self, history):
|
||||||
|
if len(history) < self.lookback:
|
||||||
|
return None
|
||||||
|
|
||||||
|
window = history[-self.lookback:]
|
||||||
|
highest = max(c.high for c in window)
|
||||||
|
lowest = min(c.low for c in window)
|
||||||
|
full_range = highest - lowest
|
||||||
|
|
||||||
|
# Check how much of the range was used early vs late
|
||||||
|
first_half = window[:len(window) // 2]
|
||||||
|
second_half = window[len(window) // 2:]
|
||||||
|
|
||||||
|
first_high = max(c.high for c in first_half)
|
||||||
|
first_low = min(c.low for c in first_half)
|
||||||
|
second_high = max(c.high for c in second_half)
|
||||||
|
second_low = min(c.low for c in second_half)
|
||||||
|
|
||||||
|
# If second half is expanding beyond first half range, it's direction
|
||||||
|
expansion = 0
|
||||||
|
if second_high > first_high:
|
||||||
|
expansion += second_high - first_high
|
||||||
|
if second_low < first_low:
|
||||||
|
expansion += first_low - second_low
|
||||||
|
|
||||||
|
avg_candle = sum(self.get_atr1(c) for c in window) / len(window)
|
||||||
|
|
||||||
|
if expansion > avg_candle * self.range_threshold:
|
||||||
|
return "direction"
|
||||||
|
return "consolidation"
|
||||||
|
|
||||||
|
def check_signal(self, history):
|
||||||
|
if len(history) < self.lookback + 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
category = self.classify(history)
|
||||||
|
if category is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
window = history[-self.lookback:]
|
||||||
|
highest = max(c.high for c in window)
|
||||||
|
lowest = min(c.low for c in window)
|
||||||
|
mid = (highest + lowest) / 2
|
||||||
|
candle = history[-1]
|
||||||
|
prev = history[-2]
|
||||||
|
|
||||||
|
atr = self.get_atr1(candle)
|
||||||
|
bracket = atr * self.atr_multiplier
|
||||||
|
|
||||||
|
if category == "consolidation":
|
||||||
|
# Near top of range and candle turning down: sell
|
||||||
|
if candle.close > mid and candle.close < prev.close:
|
||||||
|
return "SELL"
|
||||||
|
# Near bottom of range and candle turning up: buy
|
||||||
|
if candle.close < mid and candle.close > prev.close:
|
||||||
|
return "BUY"
|
||||||
|
|
||||||
|
elif category == "direction":
|
||||||
|
# Price pushing up: follow
|
||||||
|
if candle.close > prev.close and candle.close > mid:
|
||||||
|
return "BUY"
|
||||||
|
# Price pushing down: follow
|
||||||
|
if candle.close < prev.close and candle.close < mid:
|
||||||
|
return "SELL"
|
||||||
|
|
||||||
|
return None
|
||||||
+230
-284
@@ -15,6 +15,8 @@ const TIMEFRAMES = [
|
|||||||
{ label: "1H", value: 60 },
|
{ label: "1H", value: 60 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const RR_OPTIONS = [1.0, 1.5, 2.0, 2.5, 3.0];
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
bg: "#0a0a12",
|
bg: "#0a0a12",
|
||||||
surface: "#12121e",
|
surface: "#12121e",
|
||||||
@@ -34,15 +36,22 @@ const COLORS = {
|
|||||||
obBearish: "#f59e0b",
|
obBearish: "#f59e0b",
|
||||||
fvg: "#a855f7",
|
fvg: "#a855f7",
|
||||||
liquidity: "#06b6d4",
|
liquidity: "#06b6d4",
|
||||||
|
tradeWin: "#22c55e",
|
||||||
|
tradeLoss: "#ef4444",
|
||||||
|
equity: "#6366f1",
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const chartContainerRef = useRef(null);
|
const chartContainerRef = useRef(null);
|
||||||
|
const equityChartRef = useRef(null);
|
||||||
const chartRef = useRef(null);
|
const chartRef = useRef(null);
|
||||||
|
const equityChartObjRef = useRef(null);
|
||||||
const candleSeriesRef = useRef(null);
|
const candleSeriesRef = useRef(null);
|
||||||
const markersRef = useRef(null);
|
const markersRef = useRef(null);
|
||||||
|
|
||||||
const [timeframe, setTimeframe] = useState(5);
|
const [timeframe, setTimeframe] = useState(5);
|
||||||
|
const [riskReward, setRiskReward] = useState(2.5);
|
||||||
|
const [showBacktest, setShowBacktest] = useState(true);
|
||||||
const [indicators, setIndicators] = useState({
|
const [indicators, setIndicators] = useState({
|
||||||
structure: true,
|
structure: true,
|
||||||
orderBlocks: true,
|
orderBlocks: true,
|
||||||
@@ -50,6 +59,7 @@ function App() {
|
|||||||
liquidity: false,
|
liquidity: false,
|
||||||
});
|
});
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
|
const [backtestStats, setBacktestStats] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const toggleIndicator = (key) => {
|
const toggleIndicator = (key) => {
|
||||||
@@ -59,12 +69,20 @@ function App() {
|
|||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [candleRes, indicatorRes] = await Promise.all([
|
const fetches = [
|
||||||
fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}`),
|
fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}`),
|
||||||
fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}`),
|
fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}`),
|
||||||
]);
|
];
|
||||||
const candleData = await candleRes.json();
|
if (showBacktest) {
|
||||||
const indicatorData = await indicatorRes.json();
|
fetches.push(
|
||||||
|
fetch(`http://localhost:8000/api/backtest?timeframe=${timeframe}&rr=${riskReward}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.all(fetches);
|
||||||
|
const candleData = await responses[0].json();
|
||||||
|
const indicatorData = await responses[1].json();
|
||||||
|
const backtestData = showBacktest ? await responses[2].json() : null;
|
||||||
|
|
||||||
const formatted = candleData.candles.map((c) => ({
|
const formatted = candleData.candles.map((c) => ({
|
||||||
time: Math.floor(new Date(c.time).getTime() / 1000),
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
@@ -78,7 +96,6 @@ function App() {
|
|||||||
candleSeriesRef.current.setData(formatted);
|
candleSeriesRef.current.setData(formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build markers based on active indicators
|
|
||||||
const times = indicatorData.candle_times;
|
const times = indicatorData.candle_times;
|
||||||
const markers = [];
|
const markers = [];
|
||||||
|
|
||||||
@@ -88,10 +105,7 @@ function App() {
|
|||||||
markers.push({
|
markers.push({
|
||||||
time: Math.floor(new Date(times[s.index]).getTime() / 1000),
|
time: Math.floor(new Date(times[s.index]).getTime() / 1000),
|
||||||
position: s.type === "high" ? "aboveBar" : "belowBar",
|
position: s.type === "high" ? "aboveBar" : "belowBar",
|
||||||
color:
|
color: s.label === "HH" || s.label === "HL" ? COLORS.bullish : COLORS.bearish,
|
||||||
s.label === "HH" || s.label === "HL"
|
|
||||||
? COLORS.bullish
|
|
||||||
: COLORS.bearish,
|
|
||||||
shape: s.type === "high" ? "arrowDown" : "arrowUp",
|
shape: s.type === "high" ? "arrowDown" : "arrowUp",
|
||||||
text: s.label,
|
text: s.label,
|
||||||
});
|
});
|
||||||
@@ -143,29 +157,61 @@ function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showBacktest && backtestData && backtestData.trades) {
|
||||||
|
backtestData.trades.forEach((t) => {
|
||||||
|
const isWin = t.pnl > 0;
|
||||||
|
markers.push({
|
||||||
|
time: Math.floor(new Date(t.enter_time).getTime() / 1000),
|
||||||
|
position: t.direction === "long" ? "belowBar" : "aboveBar",
|
||||||
|
color: isWin ? COLORS.tradeWin : COLORS.tradeLoss,
|
||||||
|
shape: t.direction === "long" ? "arrowUp" : "arrowDown",
|
||||||
|
text: t.direction === "long" ? "BUY" : "SELL",
|
||||||
|
});
|
||||||
|
markers.push({
|
||||||
|
time: Math.floor(new Date(t.exit_time).getTime() / 1000),
|
||||||
|
position: "inBar",
|
||||||
|
color: isWin ? COLORS.tradeWin : COLORS.tradeLoss,
|
||||||
|
shape: "circle",
|
||||||
|
text: isWin ? `+${t.pnl.toFixed(2)}` : t.pnl.toFixed(2),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setBacktestStats(backtestData.stats);
|
||||||
|
|
||||||
|
if (equityChartObjRef.current && backtestData.trades.length > 0) {
|
||||||
|
let cumPnl = 0;
|
||||||
|
const equityData = backtestData.trades.map((t) => {
|
||||||
|
cumPnl += t.pnl;
|
||||||
|
return {
|
||||||
|
time: Math.floor(new Date(t.exit_time).getTime() / 1000),
|
||||||
|
value: parseFloat(cumPnl.toFixed(4)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const eChart = equityChartObjRef.current;
|
||||||
|
const equitySeries = eChart.addSeries(LineSeries, {
|
||||||
|
color: COLORS.equity,
|
||||||
|
lineWidth: 2,
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: true,
|
||||||
|
});
|
||||||
|
equitySeries.setData(equityData);
|
||||||
|
eChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setBacktestStats(null);
|
||||||
|
}
|
||||||
|
|
||||||
markers.sort((a, b) => a.time - b.time);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
// Remove old markers
|
|
||||||
if (markersRef.current) {
|
if (markersRef.current) {
|
||||||
markersRef.current.setMarkers([]);
|
markersRef.current.setMarkers([]);
|
||||||
}
|
}
|
||||||
markersRef.current = createSeriesMarkers(candleSeriesRef.current, markers);
|
markersRef.current = createSeriesMarkers(candleSeriesRef.current, markers);
|
||||||
|
|
||||||
chartRef.current.timeScale().fitContent();
|
chartRef.current.timeScale().fitContent();
|
||||||
|
|
||||||
// Stats
|
const bullishOB = indicatorData.order_blocks.filter((o) => o.type === "bullish").length;
|
||||||
const bullishOB = indicatorData.order_blocks.filter(
|
const bearishOB = indicatorData.order_blocks.filter((o) => o.type === "bearish").length;
|
||||||
(o) => o.type === "bullish"
|
|
||||||
).length;
|
|
||||||
const bearishOB = indicatorData.order_blocks.filter(
|
|
||||||
(o) => o.type === "bearish"
|
|
||||||
).length;
|
|
||||||
const bullishFVG = indicatorData.fvgs.filter(
|
|
||||||
(f) => f.type === "bullish"
|
|
||||||
).length;
|
|
||||||
const bearishFVG = indicatorData.fvgs.filter(
|
|
||||||
(f) => f.type === "bearish"
|
|
||||||
).length;
|
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
candles: candleData.candles.length,
|
candles: candleData.candles.length,
|
||||||
@@ -175,27 +221,24 @@ function App() {
|
|||||||
bullishOB,
|
bullishOB,
|
||||||
bearishOB,
|
bearishOB,
|
||||||
fvgs: indicatorData.fvgs.length,
|
fvgs: indicatorData.fvgs.length,
|
||||||
bullishFVG,
|
|
||||||
bearishFVG,
|
|
||||||
liquidity: indicatorData.liquidity.length,
|
liquidity: indicatorData.liquidity.length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load data:", err);
|
console.error("Failed to load data:", err);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [timeframe, indicators]);
|
}, [timeframe, indicators, showBacktest, riskReward]);
|
||||||
|
|
||||||
// Create chart once
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current) return;
|
if (!chartContainerRef.current) return;
|
||||||
|
|
||||||
const chart = createChart(chartContainerRef.current, {
|
const chart = createChart(chartContainerRef.current, {
|
||||||
width: chartContainerRef.current.clientWidth,
|
width: chartContainerRef.current.clientWidth,
|
||||||
height: 560,
|
height: 480,
|
||||||
layout: {
|
layout: {
|
||||||
background: { color: COLORS.surface },
|
background: { color: COLORS.surface },
|
||||||
textColor: COLORS.textDim,
|
textColor: COLORS.textDim,
|
||||||
fontFamily: "'IBM Plex', 'Fira Code', monospace",
|
fontFamily: "'IBM Plex Mono', monospace",
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -203,24 +246,11 @@ function App() {
|
|||||||
horzLines: { color: COLORS.border },
|
horzLines: { color: COLORS.border },
|
||||||
},
|
},
|
||||||
crosshair: {
|
crosshair: {
|
||||||
vertLine: {
|
vertLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent },
|
||||||
color: "rgba(99, 102, 241, 0.3)",
|
horzLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent },
|
||||||
labelBackgroundColor: COLORS.accent,
|
|
||||||
},
|
|
||||||
horzLine: {
|
|
||||||
color: "rgba(99, 102, 241, 0.3)",
|
|
||||||
labelBackgroundColor: COLORS.accent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rightPriceScale: {
|
|
||||||
borderColor: COLORS.border,
|
|
||||||
textColor: COLORS.textDim,
|
|
||||||
},
|
|
||||||
timeScale: {
|
|
||||||
borderColor: COLORS.border,
|
|
||||||
timeVisible: true,
|
|
||||||
secondsVisible: false,
|
|
||||||
},
|
},
|
||||||
|
rightPriceScale: { borderColor: COLORS.border, textColor: COLORS.textDim },
|
||||||
|
timeScale: { borderColor: COLORS.border, timeVisible: true, secondsVisible: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const candleSeries = chart.addSeries(CandlestickSeries, {
|
const candleSeries = chart.addSeries(CandlestickSeries, {
|
||||||
@@ -234,26 +264,47 @@ function App() {
|
|||||||
chartRef.current = chart;
|
chartRef.current = chart;
|
||||||
candleSeriesRef.current = candleSeries;
|
candleSeriesRef.current = candleSeries;
|
||||||
|
|
||||||
|
if (equityChartRef.current) {
|
||||||
|
const eChart = createChart(equityChartRef.current, {
|
||||||
|
width: equityChartRef.current.clientWidth,
|
||||||
|
height: 160,
|
||||||
|
layout: {
|
||||||
|
background: { color: COLORS.surface },
|
||||||
|
textColor: COLORS.textDim,
|
||||||
|
fontFamily: "'IBM Plex Mono', monospace",
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: COLORS.border },
|
||||||
|
horzLines: { color: COLORS.border },
|
||||||
|
},
|
||||||
|
rightPriceScale: { borderColor: COLORS.border },
|
||||||
|
timeScale: { borderColor: COLORS.border, timeVisible: true, secondsVisible: false },
|
||||||
|
crosshair: {
|
||||||
|
vertLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent },
|
||||||
|
horzLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
equityChartObjRef.current = eChart;
|
||||||
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (chartContainerRef.current) {
|
if (chartContainerRef.current)
|
||||||
chart.applyOptions({
|
chart.applyOptions({ width: chartContainerRef.current.clientWidth });
|
||||||
width: chartContainerRef.current.clientWidth,
|
if (equityChartRef.current && equityChartObjRef.current)
|
||||||
});
|
equityChartObjRef.current.applyOptions({ width: equityChartRef.current.clientWidth });
|
||||||
}
|
|
||||||
};
|
};
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
chart.remove();
|
chart.remove();
|
||||||
|
if (equityChartObjRef.current) equityChartObjRef.current.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load data when timeframe or indicators change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartRef.current && candleSeriesRef.current) {
|
if (chartRef.current && candleSeriesRef.current) loadData();
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -263,7 +314,6 @@ function App() {
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<header style={styles.header}>
|
<header style={styles.header}>
|
||||||
<div style={styles.headerLeft}>
|
<div style={styles.headerLeft}>
|
||||||
<div style={styles.logo}>
|
<div style={styles.logo}>
|
||||||
@@ -283,7 +333,6 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Controls */}
|
|
||||||
<div style={styles.controls}>
|
<div style={styles.controls}>
|
||||||
<div style={styles.controlGroup}>
|
<div style={styles.controlGroup}>
|
||||||
<span style={styles.controlLabel}>Timeframe</span>
|
<span style={styles.controlLabel}>Timeframe</span>
|
||||||
@@ -292,10 +341,7 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
key={tf.value}
|
key={tf.value}
|
||||||
onClick={() => setTimeframe(tf.value)}
|
onClick={() => setTimeframe(tf.value)}
|
||||||
style={{
|
style={{ ...styles.tfBtn, ...(timeframe === tf.value ? styles.tfBtnActive : {}) }}
|
||||||
...styles.tfBtn,
|
|
||||||
...(timeframe === tf.value ? styles.tfBtnActive : {}),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tf.label}
|
{tf.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -304,7 +350,22 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.controlGroup}>
|
<div style={styles.controlGroup}>
|
||||||
<span style={styles.controlLabel}>Indicators</span>
|
<span style={styles.controlLabel}>R:R</span>
|
||||||
|
<div style={styles.tfGroup}>
|
||||||
|
{RR_OPTIONS.map((rr) => (
|
||||||
|
<button
|
||||||
|
key={rr}
|
||||||
|
onClick={() => setRiskReward(rr)}
|
||||||
|
style={{ ...styles.tfBtn, ...(riskReward === rr ? styles.tfBtnActive : {}) }}
|
||||||
|
>
|
||||||
|
1:{rr}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.controlGroup}>
|
||||||
|
<span style={styles.controlLabel}>Overlays</span>
|
||||||
<div style={styles.indicatorGroup}>
|
<div style={styles.indicatorGroup}>
|
||||||
{[
|
{[
|
||||||
{ key: "structure", label: "Structure", color: COLORS.bullish },
|
{ key: "structure", label: "Structure", color: COLORS.bullish },
|
||||||
@@ -318,35 +379,92 @@ function App() {
|
|||||||
style={{
|
style={{
|
||||||
...styles.indBtn,
|
...styles.indBtn,
|
||||||
...(indicators[ind.key]
|
...(indicators[ind.key]
|
||||||
? {
|
? { borderColor: ind.color, background: `${ind.color}15`, color: ind.color }
|
||||||
borderColor: ind.color,
|
|
||||||
background: `${ind.color}15`,
|
|
||||||
color: ind.color,
|
|
||||||
}
|
|
||||||
: {}),
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
...styles.indDot,
|
...styles.indDot,
|
||||||
background: indicators[ind.key]
|
background: indicators[ind.key] ? ind.color : COLORS.textDim,
|
||||||
? ind.color
|
|
||||||
: COLORS.textDim,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ind.label}
|
{ind.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBacktest((prev) => !prev)}
|
||||||
|
style={{
|
||||||
|
...styles.indBtn,
|
||||||
|
...(showBacktest
|
||||||
|
? { borderColor: COLORS.accent, background: COLORS.accentDim, color: COLORS.accent }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{ ...styles.indDot, background: showBacktest ? COLORS.accent : COLORS.textDim }}
|
||||||
|
/>
|
||||||
|
Trades
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart */}
|
|
||||||
<div style={styles.chartWrapper}>
|
<div style={styles.chartWrapper}>
|
||||||
<div ref={chartContainerRef} style={styles.chart} />
|
<div ref={chartContainerRef} style={styles.chart} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{showBacktest && (
|
||||||
|
<div style={styles.chartWrapper}>
|
||||||
|
<div style={styles.sectionLabel}>Equity Curve</div>
|
||||||
|
<div ref={equityChartRef} style={styles.chart} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{backtestStats && showBacktest && (
|
||||||
|
<div style={styles.backtestBar}>
|
||||||
|
<div style={styles.backtestTitle}>ICT Strategy Backtest</div>
|
||||||
|
<div style={styles.backtestGrid}>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={styles.btValue}>{backtestStats.total_trades}</span>
|
||||||
|
<span style={styles.btLabel}>Trades</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: backtestStats.win_rate >= 50 ? COLORS.bullish : COLORS.bearish }}>
|
||||||
|
{backtestStats.win_rate.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
<span style={styles.btLabel}>Win Rate</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: backtestStats.total_pnl >= 0 ? COLORS.bullish : COLORS.bearish }}>
|
||||||
|
{backtestStats.total_pnl >= 0 ? "+" : ""}{backtestStats.total_pnl.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span style={styles.btLabel}>Total PnL</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: COLORS.bullish }}>{backtestStats.winners}</span>
|
||||||
|
<span style={styles.btLabel}>Winners</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: COLORS.bearish }}>{backtestStats.losers}</span>
|
||||||
|
<span style={styles.btLabel}>Losers</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: COLORS.bullish }}>+{backtestStats.avg_win.toFixed(3)}</span>
|
||||||
|
<span style={styles.btLabel}>Avg Win</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: COLORS.bearish }}>{backtestStats.avg_loss.toFixed(3)}</span>
|
||||||
|
<span style={styles.btLabel}>Avg Loss</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.btStat}>
|
||||||
|
<span style={{ ...styles.btValue, color: COLORS.accent }}>1:{backtestStats.risk_reward}</span>
|
||||||
|
<span style={styles.btLabel}>R:R</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<div style={styles.statsBar}>
|
<div style={styles.statsBar}>
|
||||||
<div style={styles.statItem}>
|
<div style={styles.statItem}>
|
||||||
@@ -360,29 +478,21 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={styles.statDivider} />
|
<div style={styles.statDivider} />
|
||||||
<div style={styles.statItem}>
|
<div style={styles.statItem}>
|
||||||
<span style={{ ...styles.statValue, color: COLORS.bullish }}>
|
<span style={{ ...styles.statValue, color: COLORS.bullish }}>{stats.bullishOB}</span>
|
||||||
{stats.bullishOB}
|
|
||||||
</span>
|
|
||||||
<span style={styles.statLabel}>Bull OB</span>
|
<span style={styles.statLabel}>Bull OB</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statItem}>
|
<div style={styles.statItem}>
|
||||||
<span style={{ ...styles.statValue, color: COLORS.bearish }}>
|
<span style={{ ...styles.statValue, color: COLORS.bearish }}>{stats.bearishOB}</span>
|
||||||
{stats.bearishOB}
|
|
||||||
</span>
|
|
||||||
<span style={styles.statLabel}>Bear OB</span>
|
<span style={styles.statLabel}>Bear OB</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statDivider} />
|
<div style={styles.statDivider} />
|
||||||
<div style={styles.statItem}>
|
<div style={styles.statItem}>
|
||||||
<span style={{ ...styles.statValue, color: COLORS.fvg }}>
|
<span style={{ ...styles.statValue, color: COLORS.fvg }}>{stats.fvgs}</span>
|
||||||
{stats.fvgs}
|
|
||||||
</span>
|
|
||||||
<span style={styles.statLabel}>FVGs</span>
|
<span style={styles.statLabel}>FVGs</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statDivider} />
|
<div style={styles.statDivider} />
|
||||||
<div style={styles.statItem}>
|
<div style={styles.statItem}>
|
||||||
<span style={{ ...styles.statValue, color: COLORS.liquidity }}>
|
<span style={{ ...styles.statValue, color: COLORS.liquidity }}>{stats.liquidity}</span>
|
||||||
{stats.liquidity}
|
|
||||||
</span>
|
|
||||||
<span style={styles.statLabel}>Liq Levels</span>
|
<span style={styles.statLabel}>Liq Levels</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,206 +501,42 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
root: {
|
root: { minHeight: "100vh", background: COLORS.bg, fontFamily: "'Outfit', sans-serif", color: COLORS.text, padding: "0" },
|
||||||
minHeight: "100vh",
|
header: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px 24px", borderBottom: `1px solid ${COLORS.border}` },
|
||||||
background: COLORS.bg,
|
headerLeft: { display: "flex", alignItems: "center", gap: "16px" },
|
||||||
fontFamily: "'Outfit', sans-serif",
|
headerRight: { display: "flex", alignItems: "center", gap: "12px" },
|
||||||
color: COLORS.text,
|
logo: { display: "flex", alignItems: "center", gap: "12px" },
|
||||||
padding: "0",
|
logoIcon: { width: "36px", height: "36px", borderRadius: "8px", background: `linear-gradient(135deg, ${COLORS.accent}, #818cf8)`, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'IBM Plex Mono', monospace", fontWeight: "700", fontSize: "13px", color: "#fff", letterSpacing: "-0.5px" },
|
||||||
},
|
logoTitle: { fontSize: "16px", fontWeight: "600", color: COLORS.textBright, letterSpacing: "-0.3px" },
|
||||||
header: {
|
logoSub: { fontSize: "11px", color: COLORS.textDim, fontFamily: "'IBM Plex Mono', monospace", letterSpacing: "0.5px", textTransform: "uppercase" },
|
||||||
display: "flex",
|
pairBadge: { display: "flex", alignItems: "center", gap: "8px", padding: "6px 12px", background: COLORS.surfaceLight, borderRadius: "6px", border: `1px solid ${COLORS.border}` },
|
||||||
justifyContent: "space-between",
|
pairFlag: { fontFamily: "'IBM Plex Mono', monospace", fontWeight: "600", fontSize: "13px", color: COLORS.textBright },
|
||||||
alignItems: "center",
|
pairLabel: { fontSize: "10px", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "1px" },
|
||||||
padding: "16px 24px",
|
loadingDot: { width: "8px", height: "8px", borderRadius: "50%", background: COLORS.accent, animation: "pulse 1.5s infinite" },
|
||||||
borderBottom: `1px solid ${COLORS.border}`,
|
controls: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 24px", borderBottom: `1px solid ${COLORS.border}`, flexWrap: "wrap", gap: "12px" },
|
||||||
},
|
controlGroup: { display: "flex", alignItems: "center", gap: "10px" },
|
||||||
headerLeft: {
|
controlLabel: { fontSize: "10px", fontWeight: "500", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "1.2px", fontFamily: "'IBM Plex Mono', monospace" },
|
||||||
display: "flex",
|
tfGroup: { display: "flex", gap: "2px", background: COLORS.surface, borderRadius: "6px", padding: "2px", border: `1px solid ${COLORS.border}` },
|
||||||
alignItems: "center",
|
tfBtn: { padding: "6px 12px", fontSize: "12px", fontWeight: "500", fontFamily: "'IBM Plex Mono', monospace", color: COLORS.textDim, background: "transparent", border: "none", borderRadius: "4px", cursor: "pointer", transition: "all 0.15s ease" },
|
||||||
gap: "16px",
|
tfBtnActive: { background: COLORS.accent, color: "#fff", boxShadow: `0 0 12px ${COLORS.accentDim}` },
|
||||||
},
|
indicatorGroup: { display: "flex", gap: "6px", flexWrap: "wrap" },
|
||||||
headerRight: {
|
indBtn: { display: "flex", alignItems: "center", gap: "6px", padding: "6px 12px", fontSize: "11px", fontWeight: "500", fontFamily: "'Outfit', sans-serif", color: COLORS.textDim, background: COLORS.surface, border: `1px solid ${COLORS.border}`, borderRadius: "6px", cursor: "pointer", transition: "all 0.15s ease" },
|
||||||
display: "flex",
|
indDot: { width: "6px", height: "6px", borderRadius: "50%" },
|
||||||
alignItems: "center",
|
chartWrapper: { padding: "12px 24px" },
|
||||||
gap: "12px",
|
chart: { borderRadius: "8px", overflow: "hidden", border: `1px solid ${COLORS.border}` },
|
||||||
},
|
sectionLabel: { fontSize: "10px", fontWeight: "500", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "1.2px", fontFamily: "'IBM Plex Mono', monospace", marginBottom: "8px" },
|
||||||
logo: {
|
backtestBar: { margin: "0 24px 16px", padding: "16px 20px", background: COLORS.surface, borderRadius: "8px", border: `1px solid ${COLORS.border}` },
|
||||||
display: "flex",
|
backtestTitle: { fontSize: "11px", fontWeight: "600", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "1.2px", fontFamily: "'IBM Plex Mono', monospace", marginBottom: "12px" },
|
||||||
alignItems: "center",
|
backtestGrid: { display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "12px" },
|
||||||
gap: "12px",
|
btStat: { display: "flex", flexDirection: "column", alignItems: "center", gap: "4px" },
|
||||||
},
|
btValue: { fontFamily: "'IBM Plex Mono', monospace", fontSize: "16px", fontWeight: "600", color: COLORS.textBright },
|
||||||
logoIcon: {
|
btLabel: { fontSize: "9px", fontWeight: "500", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "0.8px" },
|
||||||
width: "36px",
|
statsBar: { display: "flex", alignItems: "center", gap: "20px", padding: "14px 24px", margin: "0 24px 24px", background: COLORS.surface, borderRadius: "8px", border: `1px solid ${COLORS.border}`, flexWrap: "wrap" },
|
||||||
height: "36px",
|
statItem: { display: "flex", flexDirection: "column", alignItems: "center", gap: "2px" },
|
||||||
borderRadius: "8px",
|
statValue: { fontFamily: "'IBM Plex Mono', monospace", fontSize: "15px", fontWeight: "600", color: COLORS.textBright },
|
||||||
background: `linear-gradient(135deg, ${COLORS.accent}, #818cf8)`,
|
statLabel: { fontSize: "9px", fontWeight: "500", color: COLORS.textDim, textTransform: "uppercase", letterSpacing: "1px" },
|
||||||
display: "flex",
|
statDivider: { width: "1px", height: "28px", background: COLORS.border },
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
fontWeight: "700",
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "#fff",
|
|
||||||
letterSpacing: "-0.5px",
|
|
||||||
},
|
|
||||||
logoTitle: {
|
|
||||||
fontSize: "16px",
|
|
||||||
fontWeight: "600",
|
|
||||||
color: COLORS.textBright,
|
|
||||||
letterSpacing: "-0.3px",
|
|
||||||
},
|
|
||||||
logoSub: {
|
|
||||||
fontSize: "11px",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
letterSpacing: "0.5px",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
},
|
|
||||||
pairBadge: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
padding: "6px 12px",
|
|
||||||
background: COLORS.surfaceLight,
|
|
||||||
borderRadius: "6px",
|
|
||||||
border: `1px solid ${COLORS.border}`,
|
|
||||||
},
|
|
||||||
pairFlag: {
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
fontWeight: "600",
|
|
||||||
fontSize: "13px",
|
|
||||||
color: COLORS.textBright,
|
|
||||||
},
|
|
||||||
pairLabel: {
|
|
||||||
fontSize: "10px",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "1px",
|
|
||||||
},
|
|
||||||
loadingDot: {
|
|
||||||
width: "8px",
|
|
||||||
height: "8px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
background: COLORS.accent,
|
|
||||||
animation: "pulse 1.5s infinite",
|
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: "12px 24px",
|
|
||||||
borderBottom: `1px solid ${COLORS.border}`,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "12px",
|
|
||||||
},
|
|
||||||
controlGroup: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "10px",
|
|
||||||
},
|
|
||||||
controlLabel: {
|
|
||||||
fontSize: "10px",
|
|
||||||
fontWeight: "500",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "1.2px",
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
},
|
|
||||||
tfGroup: {
|
|
||||||
display: "flex",
|
|
||||||
gap: "2px",
|
|
||||||
background: COLORS.surface,
|
|
||||||
borderRadius: "6px",
|
|
||||||
padding: "2px",
|
|
||||||
border: `1px solid ${COLORS.border}`,
|
|
||||||
},
|
|
||||||
tfBtn: {
|
|
||||||
padding: "6px 12px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "500",
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
background: "transparent",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.15s ease",
|
|
||||||
},
|
|
||||||
tfBtnActive: {
|
|
||||||
background: COLORS.accent,
|
|
||||||
color: "#fff",
|
|
||||||
boxShadow: `0 0 12px ${COLORS.accentDim}`,
|
|
||||||
},
|
|
||||||
indicatorGroup: {
|
|
||||||
display: "flex",
|
|
||||||
gap: "6px",
|
|
||||||
},
|
|
||||||
indBtn: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "6px",
|
|
||||||
padding: "6px 12px",
|
|
||||||
fontSize: "11px",
|
|
||||||
fontWeight: "500",
|
|
||||||
fontFamily: "'Outfit', sans-serif",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
background: COLORS.surface,
|
|
||||||
border: `1px solid ${COLORS.border}`,
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.15s ease",
|
|
||||||
},
|
|
||||||
indDot: {
|
|
||||||
width: "6px",
|
|
||||||
height: "6px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
},
|
|
||||||
chartWrapper: {
|
|
||||||
padding: "16px 24px",
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
borderRadius: "8px",
|
|
||||||
overflow: "hidden",
|
|
||||||
border: `1px solid ${COLORS.border}`,
|
|
||||||
},
|
|
||||||
statsBar: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "20px",
|
|
||||||
padding: "14px 24px",
|
|
||||||
margin: "0 24px 24px",
|
|
||||||
background: COLORS.surface,
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `1px solid ${COLORS.border}`,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
},
|
|
||||||
statItem: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "2px",
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
fontFamily: "'IBM Plex', monospace",
|
|
||||||
fontSize: "15px",
|
|
||||||
fontWeight: "600",
|
|
||||||
color: COLORS.textBright,
|
|
||||||
},
|
|
||||||
statLabel: {
|
|
||||||
fontSize: "9px",
|
|
||||||
fontWeight: "500",
|
|
||||||
color: COLORS.textDim,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "1px",
|
|
||||||
},
|
|
||||||
statDivider: {
|
|
||||||
width: "1px",
|
|
||||||
height: "28px",
|
|
||||||
background: COLORS.border,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
Reference in New Issue
Block a user