Strategy: ORB-first + STRATEGY_SETUPS config; harness gapper filter + per-setup attribution
Sweep over 172 real gapper sessions (16 days) with a per-setup breakdown found the opening-range breakout is the ONLY profitable setup (+0.25R); every slow pattern loses (bull_flag -$78k @ ~42min is the worst). Running ORB-ONLY flips the book from -$95k to +$7.4k AND enters ~11 min earlier (23 vs 34 min avg). - classify_setup prefers the ORB when valid and honors STRATEGY_SETUPS env (comma-sep setup values) to restrict active setups; .env defaults to STRATEGY_SETUPS=opening_range_break (ORB-only). - BacktestEngine Trade.setup_type captured for per-setup attribution. - learn_params load_sessions filters to gapper sessions (mimics live scanner) so the backtest trades the same universe the system would. 75/75 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
14cccc5d30
commit
92985938e1
4 files changed
+42
-14
No files matched your search
@@ -72,6 +72,9 @@ WATCHER_SYMBOLS=AAPL,TSLA,NVDA,AMD,SOFI
|
||||
# Price band for auto-discovered candidates (Ross Cameron default 1-20).
|
||||
WATCHER_PRICE_MIN=1
|
||||
WATCHER_PRICE_MAX=20
|
||||
# Active strategy setups (comma-separated). The backtest sweep found the
|
||||
# opening-range breakout is the only profitable one; empty = all setups.
|
||||
STRATEGY_SETUPS=opening_range_break
|
||||
|
||||
# Live minute-bar ingestion (feeds the watcher with active market data)
|
||||
LIVE_BARS_ENABLED=1
|
||||
|
||||
+17
-7
@@ -49,9 +49,19 @@ def load_sessions(con):
|
||||
bars = bars[bars["is_regular_hours"] == True].reset_index(drop=True) # noqa: E712
|
||||
if len(bars) < 20:
|
||||
continue
|
||||
out.append((symbol, sess, bars,
|
||||
query_previous_close(con, symbol, sess),
|
||||
query_avg_daily_volume(con, symbol, sess)))
|
||||
pc = query_previous_close(con, symbol, sess)
|
||||
adv = query_avg_daily_volume(con, symbol, sess)
|
||||
# Mimic the LIVE scanner: only keep sessions that actually gapped up or
|
||||
# traded elevated volume. Otherwise the backtest trades non-movers the
|
||||
# live discovery would never surface, burying the real gappers' edge.
|
||||
try:
|
||||
gap = (float(bars["open"].iloc[0]) - pc) / pc if pc else 0.0
|
||||
day_rvol = (float(bars["volume"].sum()) / adv) if adv else 0.0
|
||||
except Exception: # noqa: BLE001
|
||||
gap, day_rvol = 0.0, 0.0
|
||||
if not (gap >= 0.03 or day_rvol >= 2.0):
|
||||
continue
|
||||
out.append((symbol, sess, bars, pc, adv))
|
||||
return out
|
||||
|
||||
|
||||
@@ -99,10 +109,10 @@ def main():
|
||||
"Backfill more RTH sessions for a robust optimum.\n")
|
||||
|
||||
grid = {
|
||||
"warmup": [3, 5, 8, 12],
|
||||
"min_bars": [3, 5, 8],
|
||||
"eval_every": [1, 3],
|
||||
"ready": [50, 60],
|
||||
"warmup": [3, 6, 12],
|
||||
"min_bars": [3, 6],
|
||||
"eval_every": [1, 5],
|
||||
"ready": [55, 65],
|
||||
"gap_min": [0.05],
|
||||
"rvol_min": [2.0],
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class Trade:
|
||||
exit_reason: str | None = None
|
||||
realized_pnl: float | None = None
|
||||
r_multiple: float | None = None
|
||||
setup_type: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -175,6 +176,7 @@ class BacktestEngine:
|
||||
stop_price=round(stop, 4),
|
||||
target_price=round(entry + self.target_r * risk, 4),
|
||||
quantity=sizing.position_size,
|
||||
setup_type=setup.get("setup_type"),
|
||||
)
|
||||
open_trade.realized_pnl = -commission
|
||||
i += 1 # consumed next bar for entry
|
||||
|
||||
@@ -4,6 +4,7 @@ Implements bull flag, first pullback, and gap-and-go pattern detection
|
||||
based on Ross Cameron momentum strategy specifications.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
@@ -609,24 +610,36 @@ def classify_setup(
|
||||
Returns:
|
||||
StructureDetectionResult for best valid setup
|
||||
"""
|
||||
# The opening-range breakout is the early, profitable setup — the parameter
|
||||
# sweep over real gapper sessions found it is the ONLY setup with positive
|
||||
# expectancy (+0.25R), while the slower patterns (bull_flag, continuation,
|
||||
# gap_and_go) lose and fire ~15-20 min later. So prefer it whenever it's
|
||||
# valid, rather than letting a higher-"quality"-but-slower pattern override
|
||||
# an early breakout.
|
||||
# STRATEGY_SETUPS (comma-separated setup values) restricts which setups may
|
||||
# fire. The sweep showed opening_range_break is the only profitable one, so
|
||||
# set STRATEGY_SETUPS=opening_range_break to trade ORB-only. Empty = all.
|
||||
enabled = {s.strip() for s in os.environ.get("STRATEGY_SETUPS", "").split(",") if s.strip()}
|
||||
|
||||
orb = detect_opening_range_breakout(bars)
|
||||
if (orb.is_valid and orb.quality_score >= min_quality_score
|
||||
and (not enabled or "opening_range_break" in enabled)):
|
||||
return orb
|
||||
|
||||
detectors = [
|
||||
detect_opening_range_breakout(bars),
|
||||
detect_bull_flag(bars, min_impulse_pct=0.015),
|
||||
detect_first_pullback(bars, max_pullback_depth_pct=0.6),
|
||||
detect_gap_and_go(bars, premarket_high, min_gap_pct=0.02),
|
||||
detect_hod_break(bars, session_high),
|
||||
detect_continuation_fallback(bars),
|
||||
]
|
||||
|
||||
# Filter valid setups meeting quality threshold
|
||||
valid_setups = [
|
||||
s for s in detectors if s.is_valid and s.quality_score >= min_quality_score
|
||||
s for s in detectors
|
||||
if s.is_valid and s.quality_score >= min_quality_score
|
||||
and (not enabled or s.setup_type.value in enabled)
|
||||
]
|
||||
|
||||
if not valid_setups:
|
||||
return StructureDetectionResult(
|
||||
setup_type=SetupType.NONE, is_valid=False, reason="No valid setup detected"
|
||||
)
|
||||
|
||||
# Return highest quality setup
|
||||
return max(valid_setups, key=lambda s: s.quality_score)
|
||||
Reference in new issue
Block a user