diff --git a/backend/api/routes.py b/backend/api/routes.py index 73903fb..b5941ac 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -10,7 +10,7 @@ from fastapi import FastAPI, HTTPException, Query from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field - +from indicators.sessions import set_timezone BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if BACKEND_DIR not in sys.path: sys.path.insert(0, BACKEND_DIR) @@ -524,6 +524,10 @@ def get_backtest( max_consecutive_losses: int = 0, ): dataset_id = _resolve_dataset(dataset) + if "MT5" in dataset.upper(): + set_timezone("mt5") + else: + set_timezone("est") candles = _get_candles_for_timeframe(dataset_id, timeframe) strategy = _build_strategy( diff --git a/backend/data/model.py b/backend/data/model.py index a922b1c..fa92059 100644 --- a/backend/data/model.py +++ b/backend/data/model.py @@ -28,5 +28,3 @@ class Trade: exit_price: float pnl: float r_multiple: float = 0.0 - partial_tp_taken: bool = False - partial_tp_realized_pnl: float = 0.0 diff --git a/backend/indicators/sessions.py b/backend/indicators/sessions.py index 16b23bb..0847593 100644 --- a/backend/indicators/sessions.py +++ b/backend/indicators/sessions.py @@ -1,5 +1,6 @@ from datetime import time +# EST sessions (for histdata CSVs) SESSIONS_EST = { "asian": (time(19, 0), time(3, 0)), "london": (time(2, 0), time(5, 0)), @@ -8,25 +9,42 @@ SESSIONS_EST = { "london_ny_overlap": (time(8, 0), time(10, 0)), } +# UTC+2 sessions (for MetaTrader exported CSVs) +SESSIONS_MT5 = { + "asian": (time(2, 0), time(10, 0)), + "london": (time(9, 0), time(12, 0)), + "new_york": (time(14, 0), time(17, 0)), + "london_close": (time(17, 0), time(19, 0)), + "london_ny_overlap": (time(15, 0), time(17, 0)), +} + +# Active session map (switch based on data source) +_active_sessions = SESSIONS_EST + + +def set_timezone(tz="est"): + global _active_sessions + if tz.lower() in ("mt5", "utc+2", "server"): + _active_sessions = SESSIONS_MT5 + else: + _active_sessions = SESSIONS_EST + def in_session(candle_time, session_name): if session_name == "all": return True - - if session_name not in SESSIONS_EST: + if session_name not in _active_sessions: return True t = candle_time.time() - start, end = SESSIONS_EST[session_name] + start, end = _active_sessions[session_name] if start > end: return t >= start or t < end return start <= t < end def get_session(candle_time): - for name in SESSIONS_EST: - if name == "all": - continue + for name in _active_sessions: if in_session(candle_time, name): return name return "off_hours" diff --git a/frontend/src/components/BacktestingTab.jsx b/frontend/src/components/BacktestingTab.jsx index 5c35561..dae98a9 100644 --- a/frontend/src/components/BacktestingTab.jsx +++ b/frontend/src/components/BacktestingTab.jsx @@ -39,6 +39,8 @@ const DAYS = [ const STARTING_BALANCE = 10000; const PRESETS_KEY = 'nq_backtest_presets'; const RESULT_HISTORY_KEY = 'nq_backtest_recent_results'; +const RESULT_HISTORY_LIMIT = 10; +const DEFAULT_PRESET_NAME = 'Manual'; function formatMoney(v) { return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); @@ -81,15 +83,14 @@ function savePresets(presets) { function loadRecentResults() { try { const parsed = JSON.parse(localStorage.getItem(RESULT_HISTORY_KEY) || '[]'); - return Array.isArray(parsed) ? parsed.slice(0, - 5) : []; + return Array.isArray(parsed) ? parsed.slice(0, RESULT_HISTORY_LIMIT) : []; } catch { return []; } } function saveRecentResults(results) { - localStorage.setItem(RESULT_HISTORY_KEY, JSON.stringify(results.slice(0, 5))); + localStorage.setItem(RESULT_HISTORY_KEY, JSON.stringify(results.slice(0, RESULT_HISTORY_LIMIT))); } function NumberInput({ label, value, onChange, min, max, step = 1 }) { @@ -195,6 +196,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange // Presets const [presets, setPresets] = useState(loadPresets); const [presetName, setPresetName] = useState(''); + const [activePresetName, setActivePresetName] = useState(DEFAULT_PRESET_NAME); const [showPresets, setShowPresets] = useState(false); const [recentResults, setRecentResults] = useState(loadRecentResults); @@ -243,12 +245,16 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange const updated = { ...presets, [name]: getSettings() }; setPresets(updated); savePresets(updated); + setActivePresetName(name); setPresetName(''); }; const handleLoadPreset = (name) => { const preset = presets[name]; - if (preset) applySettings(preset); + if (preset) { + applySettings(preset); + setActivePresetName(name); + } setShowPresets(false); }; @@ -257,6 +263,41 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange delete updated[name]; setPresets(updated); savePresets(updated); + if (activePresetName === name) { + setActivePresetName(DEFAULT_PRESET_NAME); + } + }; + + const exportRunParameters = (run) => { + const exportPayload = { + exportedAt: new Date().toISOString(), + runId: run.id, + presetName: run.presetName || DEFAULT_PRESET_NAME, + parameters: run.settings || { + timeframe: run.timeframe, + riskReward: run.riskReward, + }, + queryParameters: run.queryParameters || null, + summary: { + dataset: run.dataset, + timeframe: run.timeframe, + riskReward: run.riskReward, + totalPnl: run.totalPnl, + winRate: run.winRate, + totalTrades: run.totalTrades, + }, + }; + + const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + const safePreset = (run.presetName || DEFAULT_PRESET_NAME).replace(/[^a-z0-9_-]/gi, '_'); + anchor.href = url; + anchor.download = `backtest-params-${run.dataset || 'dataset'}-${safePreset}-${run.id}.json`; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); }; const toggleDay = (day) => { @@ -426,18 +467,25 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange onBacktestComplete?.(backtestData); if (backtestData?.stats) { + const snapshotSettings = { + ...getSettings(), + dayFilter: [...dayFilter], + }; const snapshot = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, runAt: new Date().toISOString(), dataset: selectedDataset, + presetName: activePresetName, timeframe, riskReward, + settings: snapshotSettings, + queryParameters: Object.fromEntries(params.entries()), totalPnl: Number(backtestData.stats.total_pnl ?? 0), winRate: Number(backtestData.stats.win_rate ?? 0), totalTrades: Number(backtestData.stats.total_trades ?? 0), }; setRecentResults((prev) => { - const next = [snapshot, ...prev].slice(0, 5); + const next = [snapshot, ...prev].slice(0, RESULT_HISTORY_LIMIT); saveRecentResults(next); return next; }); @@ -463,7 +511,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange requireFvgObConfluence, asianSweepOnly, dayFilter, useBreakEven, beTriggerRr, usePartialTp, partialTpRr, partialTpPercent, - maxDailyLoss, maxConsecutiveLosses, onBacktestComplete, + maxDailyLoss, maxConsecutiveLosses, onBacktestComplete, activePresetName, ]); const runMonteCarlo = useCallback(async () => { @@ -796,22 +844,30 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange

Results

Backtest Summary

CSV: {selectedDataset}

+

Preset: {activePresetName}

-

Last 5 Runs

+

Last {RESULT_HISTORY_LIMIT} Runs

{recentResults.length === 0 ? (

No previous runs saved yet.

) : (
{recentResults.map((run) => ( -
+
{new Date(run.runAt).toLocaleString()} - {run.dataset} - {run.timeframe}m - = 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>${formatMoney(run.totalPnl)} - {run.winRate.toFixed(1)}% - {run.totalTrades} trades + {run.dataset || 'Unknown CSV'} + {run.timeframe ?? '-'}m + {run.presetName || DEFAULT_PRESET_NAME} + = 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>${formatMoney(Number(run.totalPnl ?? 0))} + {Number(run.winRate ?? 0).toFixed(1)}% + {Number(run.totalTrades ?? 0)} trades +
))}
diff --git a/frontend/src/components/OptimizerTab.jsx b/frontend/src/components/OptimizerTab.jsx index 2ce95ba..ab19d82 100644 --- a/frontend/src/components/OptimizerTab.jsx +++ b/frontend/src/components/OptimizerTab.jsx @@ -100,9 +100,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, const [asianSweepOnlyModes, setAsianSweepOnlyModes] = useState('true,false'); const [useBreakEvenModes, setUseBreakEvenModes] = useState('false,true'); const [beTriggerRrValues, setBeTriggerRrValues] = useState('1.0,1.5,2.0'); - const [usePartialTpModes, setUsePartialTpModes] = useState('false,true'); - const [partialTpRrValues, setPartialTpRrValues] = useState('1.0,1.5,2.0'); - const [partialTpPercentValues, setPartialTpPercentValues] = useState('50'); const [loading, setLoading] = useState(false); const [progressPct, setProgressPct] = useState(0); @@ -158,9 +155,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asianSweepOnly: params.asian_sweep_only === true || params.asian_sweep_only === 'true', useBreakEven: params.use_break_even === true || params.use_break_even === 'true', beTriggerRr: Number(params.be_trigger_rr ?? 1.0), - usePartialTp: params.use_partial_tp === true || params.use_partial_tp === 'true', - partialTpRr: Number(params.partial_tp_rr ?? 1.0), - partialTpPercent: Number(params.partial_tp_percent ?? 50), dayFilter: [0, 1, 2, 3, 4], maxDailyLoss: 0, maxConsecutiveLosses: 0, @@ -250,9 +244,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, * count(asianSweepOnlyModes) * count(useBreakEvenModes) * count(beTriggerRrValues) - * count(usePartialTpModes) - * count(partialTpRrValues) - * count(partialTpPercentValues) * Math.max(1, sweepFactor || boolCount); }, [ sessions, @@ -271,9 +262,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asianSweepOnlyModes, useBreakEvenModes, beTriggerRrValues, - usePartialTpModes, - partialTpRrValues, - partialTpPercentValues, ]); const mcPnlHistogram = useMemo(() => { @@ -358,9 +346,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asian_sweep_only_modes: parseBoolList(asianSweepOnlyModes, [true, false]), use_break_even_modes: parseBoolList(useBreakEvenModes, [false, true]), be_trigger_rr_values: parseFloatList(beTriggerRrValues, [1.0, 1.5, 2.0]), - use_partial_tp_modes: parseBoolList(usePartialTpModes, [false, true]), - partial_tp_rr_values: parseFloatList(partialTpRrValues, [1.0, 1.5, 2.0]), - partial_tp_percent_values: parseFloatList(partialTpPercentValues, [50]), }; const seedValue = comboSamplingSeed.trim(); if (seedValue) { @@ -481,9 +466,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asianSweepOnlyModes, useBreakEvenModes, beTriggerRrValues, - usePartialTpModes, - partialTpRrValues, - partialTpPercentValues, activateTopResult, ]); @@ -523,9 +505,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asian_sweep_only: String(targetParams.asian_sweep_only ?? false), use_break_even: String(targetParams.use_break_even ?? false), be_trigger_rr: String(targetParams.be_trigger_rr ?? 1.0), - use_partial_tp: String(targetParams.use_partial_tp ?? false), - partial_tp_rr: String(targetParams.partial_tp_rr ?? 1.0), - partial_tp_percent: String(targetParams.partial_tp_percent ?? 50), runs: String(mcRuns), shuffle_trades: String(mcShuffleTrades), pnl_variation_pct: String(mcVariationPct), @@ -663,9 +642,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, asian_sweep_only: p.asianSweepOnly, use_break_even: p.useBreakEven, be_trigger_rr: p.beTriggerRr, - use_partial_tp: p.usePartialTp, - partial_tp_rr: p.partialTpRr, - partial_tp_percent: p.partialTpPercent, }, timeframe: p.timeframe, riskReward: p.riskReward, @@ -790,9 +766,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange, - - -