refactor: clean up
This commit is contained in:
@@ -614,7 +614,6 @@ def get_optimize(req: OptimizeRequest):
|
|||||||
ranking_key = req.rank_objective
|
ranking_key = req.rank_objective
|
||||||
|
|
||||||
def _rank_results(items):
|
def _rank_results(items):
|
||||||
# Lower drawdown is better; all other objectives are maximize.
|
|
||||||
if ranking_key == "max_drawdown_pct":
|
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))
|
||||||
return sorted(items, key=lambda x: x.get(ranking_key, 0), reverse=True)
|
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
|
total_generated_combinations = base_count_without_sweep * sweep_factor
|
||||||
|
|
||||||
if sample_mode == "first":
|
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))
|
combos = list(islice(_candidate_iter(), req.max_combinations))
|
||||||
else:
|
else:
|
||||||
# Reservoir sampling keeps an unbiased random sample without storing all combos.
|
|
||||||
for generated_idx, candidate in enumerate(_candidate_iter(), start=1):
|
for generated_idx, candidate in enumerate(_candidate_iter(), start=1):
|
||||||
if len(combos) < req.max_combinations:
|
if len(combos) < req.max_combinations:
|
||||||
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()
|
started = time.perf_counter()
|
||||||
results = []
|
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"
|
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):
|
for i, params in enumerate(combos):
|
||||||
@@ -924,12 +920,12 @@ def stream_backtest(
|
|||||||
use_fvg: bool = True,
|
use_fvg: bool = True,
|
||||||
use_ob: bool = True,
|
use_ob: bool = True,
|
||||||
proximity_pct: float = 0.5,
|
proximity_pct: float = 0.5,
|
||||||
# FVG Quality
|
|
||||||
min_gap_size: float = 0.0,
|
min_gap_size: float = 0.0,
|
||||||
impulse_multiplier: float = 0.0,
|
impulse_multiplier: float = 0.0,
|
||||||
require_unmitigated_fvg: bool = True,
|
require_unmitigated_fvg: bool = True,
|
||||||
require_bos_confluence: bool = False,
|
require_bos_confluence: bool = False,
|
||||||
# Order Block
|
|
||||||
min_ob_size: float = 0.0,
|
min_ob_size: float = 0.0,
|
||||||
require_fvg_ob_confluence: bool = False,
|
require_fvg_ob_confluence: bool = False,
|
||||||
# Liquidity
|
# Liquidity
|
||||||
|
|||||||
@@ -30,54 +30,6 @@ def _apply_break_even_if_triggered(position, candle, strategy):
|
|||||||
position["break_even_armed"] = True
|
position["break_even_armed"] = True
|
||||||
|
|
||||||
|
|
||||||
def _apply_partial_tp_if_triggered(position, candle, strategy):
|
|
||||||
if not position:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not getattr(strategy, "use_partial_tp", False):
|
|
||||||
return
|
|
||||||
|
|
||||||
if position.get("partial_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,
|
def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
|
||||||
max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0):
|
max_daily_loss=0.0, max_consecutive_losses=0, risk_pct=1.0):
|
||||||
trades = []
|
trades = []
|
||||||
@@ -89,7 +41,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
|
|||||||
|
|
||||||
for i, candle in enumerate(candles):
|
for i, candle in enumerate(candles):
|
||||||
if position:
|
if position:
|
||||||
_apply_partial_tp_if_triggered(position, candle, strategy)
|
|
||||||
_apply_break_even_if_triggered(position, candle, strategy)
|
_apply_break_even_if_triggered(position, candle, strategy)
|
||||||
|
|
||||||
is_long = position["direction"] == "long"
|
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
|
exit_price = sl if hit_sl else tp
|
||||||
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
|
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
|
||||||
lot_size = max(position.get("lot_size", 0.0), 0.0)
|
lot_size = max(position.get("lot_size", 0.0), 0.0)
|
||||||
remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0)
|
pnl = price_move * lot_size
|
||||||
remaining_pnl = price_move * lot_size * remaining_fraction
|
risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
|
||||||
pnl = position.get("realized_pnl", 0.0) + remaining_pnl
|
r_multiple = price_move / risk_distance
|
||||||
initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12)
|
|
||||||
r_multiple = pnl / initial_risk
|
|
||||||
|
|
||||||
trades.append(Trade(
|
trades.append(Trade(
|
||||||
enter_time=position["enter_time"],
|
enter_time=position["enter_time"],
|
||||||
@@ -116,8 +65,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
|
|||||||
exit_price=exit_price,
|
exit_price=exit_price,
|
||||||
pnl=pnl,
|
pnl=pnl,
|
||||||
r_multiple=r_multiple,
|
r_multiple=r_multiple,
|
||||||
partial_tp_taken=bool(position.get("partial_taken", False)),
|
|
||||||
partial_tp_realized_pnl=float(position.get("realized_pnl", 0.0) or 0.0),
|
|
||||||
))
|
))
|
||||||
position = None
|
position = None
|
||||||
|
|
||||||
@@ -171,10 +118,6 @@ def run_backtest(candles, strategy, starting_balance, risk_reward=1.0,
|
|||||||
"risk_distance": sl_distance,
|
"risk_distance": sl_distance,
|
||||||
"lot_size": lot_size,
|
"lot_size": lot_size,
|
||||||
"break_even_armed": False,
|
"break_even_armed": False,
|
||||||
"partial_taken": False,
|
|
||||||
"remaining_fraction": 1.0,
|
|
||||||
"realized_pnl": 0.0,
|
|
||||||
"initial_risk_amount": risk_amount,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return trades
|
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}
|
yield {"type": "progress", "processed_candles": i, "total_candles": total}
|
||||||
|
|
||||||
if position:
|
if position:
|
||||||
_apply_partial_tp_if_triggered(position, candle, strategy)
|
|
||||||
_apply_break_even_if_triggered(position, candle, strategy)
|
_apply_break_even_if_triggered(position, candle, strategy)
|
||||||
|
|
||||||
is_long = position["direction"] == "long"
|
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
|
exit_price = sl if hit_sl else tp
|
||||||
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
|
price_move = (exit_price - position["entry_price"]) if is_long else (position["entry_price"] - exit_price)
|
||||||
lot_size = max(position.get("lot_size", 0.0), 0.0)
|
lot_size = max(position.get("lot_size", 0.0), 0.0)
|
||||||
remaining_fraction = max(position.get("remaining_fraction", 1.0), 0.0)
|
pnl = price_move * lot_size
|
||||||
remaining_pnl = price_move * lot_size * remaining_fraction
|
risk_distance = max(position.get("risk_distance", 0.0), 1e-12)
|
||||||
pnl = position.get("realized_pnl", 0.0) + remaining_pnl
|
r_multiple = price_move / risk_distance
|
||||||
initial_risk = max(position.get("initial_risk_amount", 0.0), 1e-12)
|
|
||||||
r_multiple = pnl / initial_risk
|
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
enter_time=position["enter_time"],
|
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,
|
exit_price=exit_price,
|
||||||
pnl=pnl,
|
pnl=pnl,
|
||||||
r_multiple=r_multiple,
|
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
|
position = None
|
||||||
|
|
||||||
@@ -283,10 +221,6 @@ def run_backtest_stream(candles, strategy, starting_balance, risk_reward=1.0,
|
|||||||
"risk_distance": sl_distance,
|
"risk_distance": sl_distance,
|
||||||
"lot_size": lot_size,
|
"lot_size": lot_size,
|
||||||
"break_even_armed": False,
|
"break_even_armed": False,
|
||||||
"partial_taken": False,
|
|
||||||
"remaining_fraction": 1.0,
|
|
||||||
"realized_pnl": 0.0,
|
|
||||||
"initial_risk_amount": risk_amount,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield {"type": "done", "total_candles": total}
|
yield {"type": "done", "total_candles": total}
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
def find_fvgs(candles, min_gap_size=0.0, impulse_multiplier=0.0):
|
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 = []
|
fvgs = []
|
||||||
|
|
||||||
avg_body = 0
|
avg_body = 0
|
||||||
@@ -50,7 +42,6 @@ def find_fvgs(candles, min_gap_size=0.0, impulse_multiplier=0.0):
|
|||||||
"bottom": c3.high,
|
"bottom": c3.high,
|
||||||
"mitigated": False,
|
"mitigated": False,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Mark mitigated FVGs
|
# Mark mitigated FVGs
|
||||||
for fvg in fvgs:
|
for fvg in fvgs:
|
||||||
if fvg["mitigated"]:
|
if fvg["mitigated"]:
|
||||||
|
|||||||
+17
-4
@@ -14,6 +14,7 @@ import { EquityCurve } from './components/EquityCurve';
|
|||||||
import { MetricCard } from './components/MetricCard';
|
import { MetricCard } from './components/MetricCard';
|
||||||
import { PerformanceBreakdown } from './components/PerformanceBreakdown';
|
import { PerformanceBreakdown } from './components/PerformanceBreakdown';
|
||||||
import { TradeDistribution } from './components/TradeDistribution';
|
import { TradeDistribution } from './components/TradeDistribution';
|
||||||
|
import appLogo from './assets/favicon.png';
|
||||||
|
|
||||||
const TIMEFRAMES = [
|
const TIMEFRAMES = [
|
||||||
{ label: '1m', value: 1 },
|
{ label: '1m', value: 1 },
|
||||||
@@ -143,6 +144,17 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => { setMounted(true); }, []);
|
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(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
const loadDatasets = async () => {
|
const loadDatasets = async () => {
|
||||||
@@ -461,12 +473,13 @@ export default function App() {
|
|||||||
animate={mounted ? 'visible' : 'hidden'}
|
animate={mounted ? 'visible' : 'hidden'}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-10 h-10 bg-[#fafafa] text-black font-mono font-bold text-[13px] flex items-center justify-center tracking-tight">
|
<img
|
||||||
nQ
|
src={appLogo}
|
||||||
</div>
|
alt="noteQuant logo"
|
||||||
|
className="w-10 h-10 object-contain border border-[#262626] bg-black p-1"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[17px] font-semibold tracking-tight">noteQuant</div>
|
<div className="text-[17px] font-semibold tracking-tight">noteQuant</div>
|
||||||
<div className="text-[11px] text-[#737373] font-mono uppercase tracking-widest">ICT / SMC Backtester</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -81,7 +81,8 @@ 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, 5) : [];
|
return Array.isArray(parsed) ? parsed.slice(0,
|
||||||
|
5) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user