fix: harden drift verifier and validation flow
This commit is contained in:
@@ -26,7 +26,7 @@ Progressive test sweep parameters (512 combinations):
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
try:
|
||||
from typing import Literal
|
||||
@@ -36,6 +36,8 @@ except ImportError:
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from backtester.simulator import SimulationResult, TradeConfig, _simulate_trade
|
||||
|
||||
from backtester.math_utils import calculate_adx, calculate_atr, rma
|
||||
|
||||
Direction = Literal["long", "short"]
|
||||
@@ -85,6 +87,7 @@ class MoneyLineV11Signal:
|
||||
rsi: float
|
||||
volume_ratio: float
|
||||
price_position: float
|
||||
signal_type: str = "moneyline_v11"
|
||||
|
||||
|
||||
def ema(series: pd.Series, length: int) -> pd.Series:
|
||||
@@ -209,7 +212,9 @@ def supertrend_v11(df: pd.DataFrame, atr_period: int, multiplier: float,
|
||||
return pd.Series(tsl, index=df.index), pd.Series(trend, index=df.index)
|
||||
|
||||
|
||||
def money_line_v11_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV11Inputs] = None) -> list[MoneyLineV11Signal]:
|
||||
def money_line_v11_signals(
|
||||
df: pd.DataFrame, inputs: Optional[MoneyLineV11Inputs] = None
|
||||
) -> tuple[list[MoneyLineV11Signal], pd.DataFrame]:
|
||||
"""
|
||||
v11 "Money Line All Filters" signal generation.
|
||||
|
||||
@@ -229,8 +234,9 @@ def money_line_v11_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV11Inputs
|
||||
if inputs is None:
|
||||
inputs = MoneyLineV11Inputs()
|
||||
|
||||
data = df.copy()
|
||||
data = data.sort_index()
|
||||
# Work in-place on provided DataFrame so downstream consumers (e.g., two-stage
|
||||
# confirmation) can access calculated indicator columns.
|
||||
data = df.sort_index()
|
||||
|
||||
# Calculate Money Line
|
||||
supertrend, trend = supertrend_v11(
|
||||
@@ -380,4 +386,88 @@ def money_line_v11_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV11Inputs
|
||||
)
|
||||
cooldown_remaining = inputs.cooldown_bars
|
||||
|
||||
return signals
|
||||
return signals, data
|
||||
|
||||
|
||||
def _two_stage_confirmation(
|
||||
signals: list[MoneyLineV11Signal],
|
||||
df: pd.DataFrame,
|
||||
confirm_pct: float = 0.15,
|
||||
) -> list[MoneyLineV11Signal]:
|
||||
"""Filter signals with a simple two-stage confirmation on the next bar."""
|
||||
confirmed: list[MoneyLineV11Signal] = []
|
||||
index_positions = {ts: idx for idx, ts in enumerate(df.index)}
|
||||
threshold = confirm_pct / 100.0
|
||||
|
||||
for signal in signals:
|
||||
idx = index_positions.get(signal.timestamp)
|
||||
if idx is None or idx + 1 >= len(df):
|
||||
continue
|
||||
|
||||
next_bar = df.iloc[idx + 1]
|
||||
next_close = float(next_bar["close"])
|
||||
|
||||
if signal.direction == "long":
|
||||
needed = signal.entry_price * (1 + threshold)
|
||||
if next_close < needed:
|
||||
continue
|
||||
else:
|
||||
needed = signal.entry_price * (1 - threshold)
|
||||
if next_close > needed:
|
||||
continue
|
||||
|
||||
confirmed.append(
|
||||
MoneyLineV11Signal(
|
||||
timestamp=df.index[idx + 1],
|
||||
direction=signal.direction,
|
||||
entry_price=next_close,
|
||||
adx=float(next_bar["adx"]),
|
||||
atr=float(next_bar["atr"]),
|
||||
rsi=float(next_bar["rsi"]),
|
||||
volume_ratio=float(next_bar["volume_ratio"]),
|
||||
price_position=float(next_bar["price_position"]),
|
||||
)
|
||||
)
|
||||
|
||||
return confirmed
|
||||
|
||||
|
||||
def simulate_money_line_v11(
|
||||
df: pd.DataFrame,
|
||||
symbol: str,
|
||||
inputs: Optional[MoneyLineV11Inputs] = None,
|
||||
config: Optional[TradeConfig] = None,
|
||||
quality_filter: Optional[Callable[[MoneyLineV11Signal], bool]] = None,
|
||||
two_stage: bool = False,
|
||||
confirm_pct: float = 0.15,
|
||||
) -> SimulationResult:
|
||||
if inputs is None:
|
||||
inputs = MoneyLineV11Inputs()
|
||||
if config is None:
|
||||
config = TradeConfig()
|
||||
if quality_filter is None:
|
||||
quality_filter = lambda _: True # type: ignore
|
||||
|
||||
data = df.sort_index().copy()
|
||||
index_positions = {ts: idx for idx, ts in enumerate(data.index)}
|
||||
|
||||
signals, enriched = money_line_v11_signals(data, inputs)
|
||||
if two_stage:
|
||||
signals = _two_stage_confirmation(signals, enriched, confirm_pct)
|
||||
|
||||
trades = []
|
||||
next_available_index = 0
|
||||
|
||||
for signal in signals:
|
||||
if not quality_filter(signal):
|
||||
continue
|
||||
start_idx = index_positions.get(signal.timestamp)
|
||||
if start_idx is None or start_idx < next_available_index:
|
||||
continue
|
||||
trade = _simulate_trade(data, start_idx, signal, symbol, config)
|
||||
if trade is None:
|
||||
continue
|
||||
trades.append(trade)
|
||||
next_available_index = trade._exit_index
|
||||
|
||||
return SimulationResult(trades=trades)
|
||||
|
||||
Reference in New Issue
Block a user