diff --git a/backend/api/routes.py b/backend/api/routes.py index b4aa98d..73903fb 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -614,7 +614,6 @@ def get_optimize(req: OptimizeRequest): ranking_key = req.rank_objective def _rank_results(items): - # Lower drawdown is better; all other objectives are maximize. if ranking_key == "max_drawdown_pct": return sorted(items, key=lambda x: x.get(ranking_key, 0)) return sorted(items, key=lambda x: x.get(ranking_key, 0), reverse=True) @@ -666,10 +665,8 @@ def get_optimize(req: OptimizeRequest): total_generated_combinations = base_count_without_sweep * sweep_factor if sample_mode == "first": - # Stop iteration as soon as we hit the cap (no ghost work on remaining permutations). combos = list(islice(_candidate_iter(), req.max_combinations)) else: - # Reservoir sampling keeps an unbiased random sample without storing all combos. for generated_idx, candidate in enumerate(_candidate_iter(), start=1): if len(combos) < req.max_combinations: if len(combos) < req.max_combinations: @@ -686,7 +683,6 @@ def get_optimize(req: OptimizeRequest): started = time.perf_counter() results = [] - # Emit immediately so the UI can move off 0% as soon as the stream opens. yield f"data: {json.dumps({'type': 'progress', 'progress': 0, 'processed': 0, 'total_combinations': executed_combinations, 'generated_combinations': total_generated_combinations, 'executed_combinations': executed_combinations, 'max_combinations': req.max_combinations, 'capped_by_max_combinations': capped_by_max_combinations, 'combo_sampling_mode': sample_mode, 'combo_sampling_seed': req.combo_sampling_seed, 'valid_results': 0, 'top_results': []})}\n\n" for i, params in enumerate(combos): @@ -924,12 +920,12 @@ def stream_backtest( use_fvg: bool = True, use_ob: bool = True, proximity_pct: float = 0.5, - # FVG Quality + min_gap_size: float = 0.0, impulse_multiplier: float = 0.0, require_unmitigated_fvg: bool = True, require_bos_confluence: bool = False, - # Order Block + min_ob_size: float = 0.0, require_fvg_ob_confluence: bool = False, # Liquidity diff --git a/backend/engine/backtester.py b/backend/engine/backtester.py index 4a0041e..c51694d 100644 --- a/backend/engine/backtester.py +++ b/backend/engine/backtester.py @@ -30,54 +30,6 @@ def _apply_break_even_if_triggered(position, candle, strategy): position["break_even_armed"] = True -def _apply_partial_tp_if_triggered(position, candle, strategy): - if not position: - return - - if not getattr(strategy, "use_partial_tp", False): - return - - if position.get("partial_taken"): - return - - trigger_rr = float(getattr(strategy, "partial_tp_rr", 1.0) or 0.0) - if trigger_rr <= 0: - return - - partial_pct = float(getattr(strategy, "partial_tp_percent", 0.0) or 0.0) - if partial_pct <= 0: - return - - close_fraction = min(max(partial_pct / 100.0, 0.0), 1.0) - remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0) - if remaining_fraction <= 0: - position["partial_taken"] = True - return - - close_fraction = min(close_fraction, remaining_fraction) - if close_fraction <= 0: - return - - is_long = position["direction"] == "long" - entry = position["entry_price"] - risk_distance = max(position.get("risk_distance", 0.0), 0.0) - if risk_distance <= 0: - return - - trigger_price = entry + (risk_distance * trigger_rr) if is_long else entry - (risk_distance * trigger_rr) - reached_trigger = candle.high >= trigger_price if is_long else candle.low <= trigger_price - if not reached_trigger: - return - - lot_size = max(position.get("lot_size", 0.0), 0.0) - price_move = (trigger_price - entry) if is_long else (entry - trigger_price) - realized_piece = price_move * lot_size * close_fraction - - position["realized_pnl"] = position.get("realized_pnl", 0.0) + realized_piece - position["remaining_fraction"] = max(0.0, remaining_fraction - close_fraction) - position["partial_taken"] = True - - def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0): trades = [] @@ -89,7 +41,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, for i, candle in enumerate(candles): if position: - _apply_partial_tp_if_triggered(position, candle, strategy) _apply_break_even_if_triggered(position, candle, strategy) is_long = position["direction"] == "long" @@ -102,11 +53,9 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, exit_price = sl if hit_sl else tp price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price) lot_size = max(position.get("lot_size", 0.0), 0.0) - remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0) - remaining_pnl = price_move * lot_size * remaining_fraction - pnl = position.get("realized_pnl", 0.0) + remaining_pnl - initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12) - r_multiple = pnl / initial_risk + pnl = price_move * lot_size + 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"], @@ -116,8 +65,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, exit_price=exit_price, pnl=pnl, r_multiple=r_multiple, - partial_tp_taken=bool(position.get("partial_taken", False)), - partial_tp_realized_pnl=float(position.get("realized_pnl", 0.0) or 0.0), )) position = None @@ -171,10 +118,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0, "risk_distance": sl_distance, "lot_size": lot_size, "break_even_armed": False, - "partial_taken": False, - "remaining_fraction": 1.0, - "realized_pnl": 0.0, - "initial_risk_amount": risk_amount, } return trades @@ -199,7 +142,6 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0, yield {"type": "progress", "processed_candles": i, "total_candles": total} if position: - _apply_partial_tp_if_triggered(position, candle, strategy) _apply_break_even_if_triggered(position, candle, strategy) is_long = position["direction"] == "long" @@ -212,11 +154,9 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0, exit_price = sl if hit_sl else tp price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price) lot_size = max(position.get("lot_size", 0.0), 0.0) - remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0) - remaining_pnl = price_move * lot_size * remaining_fraction - pnl = position.get("realized_pnl", 0.0) + remaining_pnl - initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12) - r_multiple = pnl / initial_risk + pnl = price_move * lot_size + risk_distance = max(position.get("risk_distance", 0.0), 1e-12) + r_multiple = price_move / risk_distance trade = Trade( enter_time=position["enter_time"], @@ -226,8 +166,6 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0, exit_price=exit_price, pnl=pnl, r_multiple=r_multiple, - partial_tp_taken=bool(position.get("partial_taken", False)), - partial_tp_realized_pnl=float(position.get("realized_pnl", 0.0) or 0.0), ) position = None @@ -283,10 +221,6 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0, "risk_distance": sl_distance, "lot_size": lot_size, "break_even_armed": False, - "partial_taken": False, - "remaining_fraction": 1.0, - "realized_pnl": 0.0, - "initial_risk_amount": risk_amount, } yield {"type": "done", "total_candles": total} \ No newline at end of file diff --git a/backend/indicators/fvg.py b/backend/indicators/fvg.py index 48807a6..ec3ff63 100644 --- a/backend/indicators/fvg.py +++ b/backend/indicators/fvg.py @@ -1,12 +1,4 @@ def find_fvgs(candles, min_gap_size=0.0, impulse_multiplier=0.0): - """ - Find Fair Value Gaps in candle data. - - Args: - candles: list of Candle objects - min_gap_size: minimum gap size in price units to filter noise (0 = no filter) - impulse_multiplier: minimum body-to-avg ratio for the middle candle (0 = no filter) - """ fvgs = [] avg_body = 0 @@ -50,7 +42,6 @@ def find_fvgs(candles, min_gap_size=0.0, impulse_multiplier=0.0): "bottom": c3.high, "mitigated": False, }) - # Mark mitigated FVGs for fvg in fvgs: if fvg["mitigated"]: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7e01b49..e0c192a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,7 @@ import { EquityCurve } from './components/EquityCurve'; import { MetricCard } from './components/MetricCard'; import { PerformanceBreakdown } from './components/PerformanceBreakdown'; import { TradeDistribution } from './components/TradeDistribution'; +import appLogo from './assets/favicon.png'; const TIMEFRAMES = [ { label: '1m', value: 1 }, @@ -143,6 +144,17 @@ export default function App() { useEffect(() => { setMounted(true); }, []); + useEffect(() => { + let link = document.querySelector("link[rel='icon']"); + if (!link) { + link = document.createElement('link'); + link.rel = 'icon'; + document.head.appendChild(link); + } + link.type = 'image/png'; + link.href = appLogo; + }, []); + useEffect(() => { let isMounted = true; const loadDatasets = async () => { @@ -461,12 +473,13 @@ export default function App() { animate={mounted ? 'visible' : 'hidden'} >