enhancing backtesting functionality

This commit is contained in:
moen0
2026-04-23 23:21:15 +02:00
parent 768511e90d
commit e6e0f6f79c
5 changed files with 98 additions and 49 deletions
+5 -1
View File
@@ -10,7 +10,7 @@ from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from indicators.sessions import set_timezone
BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if BACKEND_DIR not in sys.path: if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR) sys.path.insert(0, BACKEND_DIR)
@@ -524,6 +524,10 @@ 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():
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(
-2
View File
@@ -28,5 +28,3 @@ 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
+24 -6
View File
@@ -1,5 +1,6 @@
from datetime import time from datetime import time
# EST sessions (for histdata CSVs)
SESSIONS_EST = { SESSIONS_EST = {
"asian": (time(19, 0), time(3, 0)), "asian": (time(19, 0), time(3, 0)),
"london": (time(2, 0), time(5, 0)), "london": (time(2, 0), time(5, 0)),
@@ -8,25 +9,42 @@ SESSIONS_EST = {
"london_ny_overlap": (time(8, 0), time(10, 0)), "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): def in_session(candle_time, session_name):
if session_name == "all": if session_name == "all":
return True return True
if session_name not in _active_sessions:
if session_name not in SESSIONS_EST:
return True return True
t = candle_time.time() t = candle_time.time()
start, end = SESSIONS_EST[session_name] start, end = _active_sessions[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):
for name in SESSIONS_EST: for name in _active_sessions:
if name == "all":
continue
if in_session(candle_time, name): if in_session(candle_time, name):
return name return name
return "off_hours" return "off_hours"
+69 -13
View File
@@ -39,6 +39,8 @@ const DAYS = [
const STARTING_BALANCE = 10000; const STARTING_BALANCE = 10000;
const PRESETS_KEY = 'nq_backtest_presets'; const PRESETS_KEY = 'nq_backtest_presets';
const RESULT_HISTORY_KEY = 'nq_backtest_recent_results'; const RESULT_HISTORY_KEY = 'nq_backtest_recent_results';
const RESULT_HISTORY_LIMIT = 10;
const DEFAULT_PRESET_NAME = 'Manual';
function formatMoney(v) { function formatMoney(v) {
return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
@@ -81,15 +83,14 @@ function savePresets(presets) {
function loadRecentResults() { function loadRecentResults() {
try { try {
const parsed = JSON.parse(localStorage.getItem(RESULT_HISTORY_KEY) || '[]'); const parsed = JSON.parse(localStorage.getItem(RESULT_HISTORY_KEY) || '[]');
return Array.isArray(parsed) ? parsed.slice(0, return Array.isArray(parsed) ? parsed.slice(0, RESULT_HISTORY_LIMIT) : [];
5) : [];
} catch { } catch {
return []; return [];
} }
} }
function saveRecentResults(results) { 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 }) { function NumberInput({ label, value, onChange, min, max, step = 1 }) {
@@ -195,6 +196,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
// Presets // Presets
const [presets, setPresets] = useState(loadPresets); const [presets, setPresets] = useState(loadPresets);
const [presetName, setPresetName] = useState(''); const [presetName, setPresetName] = useState('');
const [activePresetName, setActivePresetName] = useState(DEFAULT_PRESET_NAME);
const [showPresets, setShowPresets] = useState(false); const [showPresets, setShowPresets] = useState(false);
const [recentResults, setRecentResults] = useState(loadRecentResults); const [recentResults, setRecentResults] = useState(loadRecentResults);
@@ -243,12 +245,16 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
const updated = { ...presets, [name]: getSettings() }; const updated = { ...presets, [name]: getSettings() };
setPresets(updated); setPresets(updated);
savePresets(updated); savePresets(updated);
setActivePresetName(name);
setPresetName(''); setPresetName('');
}; };
const handleLoadPreset = (name) => { const handleLoadPreset = (name) => {
const preset = presets[name]; const preset = presets[name];
if (preset) applySettings(preset); if (preset) {
applySettings(preset);
setActivePresetName(name);
}
setShowPresets(false); setShowPresets(false);
}; };
@@ -257,6 +263,41 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
delete updated[name]; delete updated[name];
setPresets(updated); setPresets(updated);
savePresets(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) => { const toggleDay = (day) => {
@@ -426,18 +467,25 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
onBacktestComplete?.(backtestData); onBacktestComplete?.(backtestData);
if (backtestData?.stats) { if (backtestData?.stats) {
const snapshotSettings = {
...getSettings(),
dayFilter: [...dayFilter],
};
const snapshot = { const snapshot = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
runAt: new Date().toISOString(), runAt: new Date().toISOString(),
dataset: selectedDataset, dataset: selectedDataset,
presetName: activePresetName,
timeframe, timeframe,
riskReward, riskReward,
settings: snapshotSettings,
queryParameters: Object.fromEntries(params.entries()),
totalPnl: Number(backtestData.stats.total_pnl ?? 0), totalPnl: Number(backtestData.stats.total_pnl ?? 0),
winRate: Number(backtestData.stats.win_rate ?? 0), winRate: Number(backtestData.stats.win_rate ?? 0),
totalTrades: Number(backtestData.stats.total_trades ?? 0), totalTrades: Number(backtestData.stats.total_trades ?? 0),
}; };
setRecentResults((prev) => { setRecentResults((prev) => {
const next = [snapshot, ...prev].slice(0, 5); const next = [snapshot, ...prev].slice(0, RESULT_HISTORY_LIMIT);
saveRecentResults(next); saveRecentResults(next);
return next; return next;
}); });
@@ -463,7 +511,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
requireFvgObConfluence, asianSweepOnly, dayFilter, requireFvgObConfluence, asianSweepOnly, dayFilter,
useBreakEven, beTriggerRr, useBreakEven, beTriggerRr,
usePartialTp, partialTpRr, partialTpPercent, usePartialTp, partialTpRr, partialTpPercent,
maxDailyLoss, maxConsecutiveLosses, onBacktestComplete, maxDailyLoss, maxConsecutiveLosses, onBacktestComplete, activePresetName,
]); ]);
const runMonteCarlo = useCallback(async () => { const runMonteCarlo = useCallback(async () => {
@@ -796,22 +844,30 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
<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>
<p className="text-[12px] text-[#737373] font-mono mt-2">CSV: {selectedDataset}</p> <p className="text-[12px] text-[#737373] font-mono mt-2">CSV: {selectedDataset}</p>
<p className="text-[12px] text-[#737373] font-mono mt-1">Preset: {activePresetName}</p>
</div> </div>
<div className="mb-6 border border-[#1a1a1a] bg-black/30 p-4"> <div className="mb-6 border border-[#1a1a1a] bg-black/30 p-4">
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-3">Last 5 Runs</p> <p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-3">Last {RESULT_HISTORY_LIMIT} Runs</p>
{recentResults.length === 0 ? ( {recentResults.length === 0 ? (
<p className="text-[12px] text-[#737373] font-mono">No previous runs saved yet.</p> <p className="text-[12px] text-[#737373] font-mono">No previous runs saved yet.</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{recentResults.map((run) => ( {recentResults.map((run) => (
<div key={run.id} className="grid grid-cols-2 md:grid-cols-6 gap-2 text-[12px] font-mono border border-[#1a1a1a] bg-black/40 px-3 py-2"> <div key={run.id} className="grid grid-cols-2 md:grid-cols-8 gap-2 text-[12px] font-mono border border-[#1a1a1a] bg-black/40 px-3 py-2 items-center">
<span className="text-[#a3a3a3]">{new Date(run.runAt).toLocaleString()}</span> <span className="text-[#a3a3a3]">{new Date(run.runAt).toLocaleString()}</span>
<span className="text-[#fafafa]">{run.dataset}</span> <span className="text-[#fafafa]">{run.dataset || 'Unknown CSV'}</span>
<span className="text-[#a3a3a3]">{run.timeframe}m</span> <span className="text-[#a3a3a3]">{run.timeframe ?? '-'}m</span>
<span className={run.totalPnl >= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>${formatMoney(run.totalPnl)}</span> <span className="text-[#a78bfa]">{run.presetName || DEFAULT_PRESET_NAME}</span>
<span className="text-[#60a5fa]">{run.winRate.toFixed(1)}%</span> <span className={(run.totalPnl ?? 0) >= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>${formatMoney(Number(run.totalPnl ?? 0))}</span>
<span className="text-[#fafafa]">{run.totalTrades} trades</span> <span className="text-[#60a5fa]">{Number(run.winRate ?? 0).toFixed(1)}%</span>
<span className="text-[#fafafa]">{Number(run.totalTrades ?? 0)} trades</span>
<button
onClick={() => exportRunParameters(run)}
className="px-2 py-1 text-[11px] border border-[#262626] text-[#d4d4d8] hover:text-[#fafafa] hover:border-[#404040] transition-colors"
>
Export
</button>
</div> </div>
))} ))}
</div> </div>
-27
View File
@@ -100,9 +100,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
const [asianSweepOnlyModes, setAsianSweepOnlyModes] = useState('true,false'); const [asianSweepOnlyModes, setAsianSweepOnlyModes] = useState('true,false');
const [useBreakEvenModes, setUseBreakEvenModes] = useState('false,true'); const [useBreakEvenModes, setUseBreakEvenModes] = useState('false,true');
const [beTriggerRrValues, setBeTriggerRrValues] = useState('1.0,1.5,2.0'); 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 [loading, setLoading] = useState(false);
const [progressPct, setProgressPct] = useState(0); 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', asianSweepOnly: params.asian_sweep_only === true || params.asian_sweep_only === 'true',
useBreakEven: params.use_break_even === true || params.use_break_even === 'true', useBreakEven: params.use_break_even === true || params.use_break_even === 'true',
beTriggerRr: Number(params.be_trigger_rr ?? 1.0), 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], dayFilter: [0, 1, 2, 3, 4],
maxDailyLoss: 0, maxDailyLoss: 0,
maxConsecutiveLosses: 0, maxConsecutiveLosses: 0,
@@ -250,9 +244,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
* count(asianSweepOnlyModes) * count(asianSweepOnlyModes)
* count(useBreakEvenModes) * count(useBreakEvenModes)
* count(beTriggerRrValues) * count(beTriggerRrValues)
* count(usePartialTpModes)
* count(partialTpRrValues)
* count(partialTpPercentValues)
* Math.max(1, sweepFactor || boolCount); * Math.max(1, sweepFactor || boolCount);
}, [ }, [
sessions, sessions,
@@ -271,9 +262,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
asianSweepOnlyModes, asianSweepOnlyModes,
useBreakEvenModes, useBreakEvenModes,
beTriggerRrValues, beTriggerRrValues,
usePartialTpModes,
partialTpRrValues,
partialTpPercentValues,
]); ]);
const mcPnlHistogram = useMemo(() => { const mcPnlHistogram = useMemo(() => {
@@ -358,9 +346,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
asian_sweep_only_modes: parseBoolList(asianSweepOnlyModes, [true, false]), asian_sweep_only_modes: parseBoolList(asianSweepOnlyModes, [true, false]),
use_break_even_modes: parseBoolList(useBreakEvenModes, [false, true]), use_break_even_modes: parseBoolList(useBreakEvenModes, [false, true]),
be_trigger_rr_values: parseFloatList(beTriggerRrValues, [1.0, 1.5, 2.0]), 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(); const seedValue = comboSamplingSeed.trim();
if (seedValue) { if (seedValue) {
@@ -481,9 +466,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
asianSweepOnlyModes, asianSweepOnlyModes,
useBreakEvenModes, useBreakEvenModes,
beTriggerRrValues, beTriggerRrValues,
usePartialTpModes,
partialTpRrValues,
partialTpPercentValues,
activateTopResult, activateTopResult,
]); ]);
@@ -523,9 +505,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
asian_sweep_only: String(targetParams.asian_sweep_only ?? false), asian_sweep_only: String(targetParams.asian_sweep_only ?? false),
use_break_even: String(targetParams.use_break_even ?? false), use_break_even: String(targetParams.use_break_even ?? false),
be_trigger_rr: String(targetParams.be_trigger_rr ?? 1.0), 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), runs: String(mcRuns),
shuffle_trades: String(mcShuffleTrades), shuffle_trades: String(mcShuffleTrades),
pnl_variation_pct: String(mcVariationPct), pnl_variation_pct: String(mcVariationPct),
@@ -663,9 +642,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
asian_sweep_only: p.asianSweepOnly, asian_sweep_only: p.asianSweepOnly,
use_break_even: p.useBreakEven, use_break_even: p.useBreakEven,
be_trigger_rr: p.beTriggerRr, be_trigger_rr: p.beTriggerRr,
use_partial_tp: p.usePartialTp,
partial_tp_rr: p.partialTpRr,
partial_tp_percent: p.partialTpPercent,
}, },
timeframe: p.timeframe, timeframe: p.timeframe,
riskReward: p.riskReward, riskReward: p.riskReward,
@@ -790,9 +766,6 @@ export function OptimizerTab({ datasets = [], selectedDataset, onDatasetChange,
<TextListInput label="Asian Sweep Only" value={asianSweepOnlyModes} onChange={setAsianSweepOnlyModes} placeholder="true,false" /> <TextListInput label="Asian Sweep Only" value={asianSweepOnlyModes} onChange={setAsianSweepOnlyModes} placeholder="true,false" />
<TextListInput label="Use Break-Even" value={useBreakEvenModes} onChange={setUseBreakEvenModes} placeholder="false,true" /> <TextListInput label="Use Break-Even" value={useBreakEvenModes} onChange={setUseBreakEvenModes} placeholder="false,true" />
<TextListInput label="BE Trigger RR" value={beTriggerRrValues} onChange={setBeTriggerRrValues} placeholder="1.0,1.5,2.0" /> <TextListInput label="BE Trigger RR" value={beTriggerRrValues} onChange={setBeTriggerRrValues} placeholder="1.0,1.5,2.0" />
<TextListInput label="Use Partial TP" value={usePartialTpModes} onChange={setUsePartialTpModes} placeholder="false,true" />
<TextListInput label="Partial TP RR" value={partialTpRrValues} onChange={setPartialTpRrValues} placeholder="1.0,1.5,2.0" />
<TextListInput label="Partial TP %" value={partialTpPercentValues} onChange={setPartialTpPercentValues} placeholder="50" />
</div> </div>
<div className="mt-6 border border-[#1a1a1a] bg-black/40 p-4"> <div className="mt-6 border border-[#1a1a1a] bg-black/40 p-4">