Distills each gapper's premarket tape (first-hit price/time, pm high/low, run %, open-vs-pm-high, VWAP distance, jackknife count) into a premarket_structure table for entry-conversion analysis. Motivated by FCUV 2026-07-31: our DB captured the full $1.98->$10.49 premarket ramp while the ORB entered at $11.89 into the reversal. Pre-registered hypotheses H1-H3 locked in the module docstring before any data was examined; report command prints descriptive buckets only, with n>=30/bucket floor and Phase-2 shadow bar required for promotion. Gates nothing, trades nothing. Wired into the nightly scheduler after premarket_eval. One session scan per build (per-symbol queries were ~100x slower); partial session index created post-close only (mid-market CONCURRENTLY build deadlocked against live ingest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
47 lines
1.8 KiB
Python
47 lines
1.8 KiB
Python
"""compute_features: the pure premarket distiller (FCUV-shaped synthetic tape)."""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from research.premarket_structure import compute_features, MIN_PM_BARS
|
|
|
|
|
|
def _bar(i, o, h, low, c, v=50_000):
|
|
return {"timestamp": datetime(2026, 7, 31, 12, 13) + timedelta(minutes=i),
|
|
"open": o, "high": h, "low": low, "close": c, "volume": v}
|
|
|
|
|
|
def test_fcuv_shaped_ramp():
|
|
# $2 -> high $10.49 -> close 9:29 at $10.20, one >=5% jackknife on the way
|
|
bars = [
|
|
_bar(0, 2.00, 2.60, 1.98, 2.55),
|
|
_bar(1, 2.55, 4.25, 2.50, 4.10),
|
|
_bar(2, 4.10, 8.00, 4.00, 7.80),
|
|
_bar(3, 7.80, 10.25, 7.40, 9.60), # runs to 10.25 then drops >5% (jackknife)
|
|
_bar(4, 9.60, 10.49, 9.40, 10.20), # new high, close near high
|
|
]
|
|
f = compute_features(bars)
|
|
assert f is not None
|
|
assert f["first_price"] == 2.00
|
|
assert f["pm_high"] == 10.49
|
|
assert f["pm_close"] == 10.20
|
|
assert abs(f["pm_run_pct"] - (10.49 - 2.0) / 2.0) < 1e-6
|
|
assert f["open_vs_pm_high_pct"] > -0.05 # consolidating at high (H2 bucket)
|
|
assert f["pullbacks_5pct"] >= 1 # the 10.25 -> 9.40 knife
|
|
assert f["pm_volume"] == 250_000
|
|
|
|
|
|
def test_faded_open_bucket():
|
|
# ran to 10 early, bled to 6 into the open -> big negative open_vs_pm_high
|
|
bars = [_bar(0, 5.0, 10.0, 4.9, 9.8)] + [
|
|
_bar(i, 9.0 - i, 9.2 - i, 8.5 - i, 8.8 - i) for i in range(1, 5)
|
|
]
|
|
f = compute_features(bars)
|
|
assert f is not None
|
|
assert f["open_vs_pm_high_pct"] < -0.15
|
|
|
|
|
|
def test_thin_tape_rejected():
|
|
assert compute_features([_bar(0, 2, 2.2, 1.9, 2.1)] * (MIN_PM_BARS - 1)) is None
|
|
thin = [_bar(i, 2, 2.2, 1.9, 2.1, v=100) for i in range(MIN_PM_BARS)]
|
|
assert compute_features(thin) is None # volume floor
|