"""Replay Yahoo's available minute tape and report alerts per CME session. Sweeps a range of thresholds in a single pass rather than testing only the configured one: the useful question is where the alert rate crosses from silent to noisy, which a single number cannot show. Manual trendlines are deliberately excluded — they are user data, and a threshold calibrated against one person's drawings would not transfer. """ import asyncio from collections import Counter from app.analysis.alerts import AlertEngine from app.analysis.confluence import cluster_levels from app.analysis.horizontals import build_prior_day_levels from app.analysis.indicators import atr from app.analysis.moving_averages import build_ma_levels from app.analysis.vwap import build_vwap_level from app.bars.aggregator import Aggregator from app.bars.models import Timeframe from app.bars.session import bucket_start from app.bars.store import InMemoryBarStore from app.config import Settings from app.market.yahoo import YahooSource THRESHOLDS = (12, 16, 20, 24, 28, 32, 40) # Scores are sums of 12s and 16s, so the threshold is quantised and blunt: # several values behave identically and then it falls to zero. Cooldown is the # finer control over how often a zone price is chopping around repeats itself. COOLDOWNS = (900, 1800, 3600, 7200, 14400) async def main() -> None: settings = Settings() source = YahooSource(settings.yahoo_poll_seconds) hourly, minutes = await asyncio.gather( source.history(settings.yahoo_symbol, Timeframe.H1, range_=settings.seed_1h_range), source.history(settings.yahoo_symbol, Timeframe.M1, range_=settings.seed_1m_range), ) if not minutes: raise RuntimeError("Yahoo returned no minute tape") aggregator = Aggregator(settings.enabled_timeframes) store = InMemoryBarStore(25_000) # Keyed on (threshold, cooldown) so one replay pass measures both sweeps. combos = [(threshold, settings.alert_cooldown_seconds) for threshold in THRESHOLDS] combos += [ (settings.confluence_min_score, cooldown) for cooldown in COOLDOWNS if cooldown != settings.alert_cooldown_seconds ] engines = {combo: AlertEngine(combo[0], combo[1]) for combo in combos} counts: dict[tuple, Counter[int]] = {combo: Counter() for combo in combos} ma_levels: list = [] cutoff = minutes[0].t for source_bar in [bar for bar in hourly if bar.t < cutoff] + minutes: for bar in aggregator.update(source_bar): store.put(bar) if settings.ma_sets.get(bar.tf): ma_levels = build_ma_levels( {tf: store.get(tf) for tf in settings.ma_sets}, settings.ma_sets ) if bar.tf is not Timeframe.M1 or not bar.closed: continue atr_values = atr(store.get(Timeframe.M15), 14) atr15 = next((value for value in reversed(atr_values) if value is not None), 0.0) levels = ( ma_levels + build_prior_day_levels(store.get(Timeframe.D1), bar.c) + build_vwap_level(store.get(Timeframe.M1)) ) # Clustering is threshold-independent, so it is done once and the # result fed to every engine. clusters = cluster_levels(levels, bar.t, bar.c, atr15) session = bucket_start(bar.t, Timeframe.D1) for combo, engine in engines.items(): alerts = engine.evaluate(clusters, bar.c, atr15, bar.t, settings.yahoo_symbol) counts[combo][session] += len(alerts) sessions = sorted({session for counter in counts.values() for session in counter}) print(f"minute_bars={len(minutes)} sessions={len(sessions)}") def report(title: str, selected: list[tuple]) -> None: print(f"\n{title}") print(f"{'threshold':>9} {'cooldown':>9} {'total':>6} {'max/sess':>9} per-session") for combo in selected: per_session = [counts[combo][session] for session in sessions] configured = combo == (settings.confluence_min_score, settings.alert_cooldown_seconds) print( f"{combo[0]:>9g} {combo[1]:>9} {sum(per_session):>6} " f"{max(per_session, default=0):>9} " f"{', '.join(str(value) for value in per_session)}" f"{' <- configured' if configured else ''}" ) report( f"threshold sweep (cooldown={settings.alert_cooldown_seconds}s)", [(threshold, settings.alert_cooldown_seconds) for threshold in THRESHOLDS], ) report( f"cooldown sweep (threshold={settings.confluence_min_score:g})", sorted( {(settings.confluence_min_score, cooldown) for cooldown in COOLDOWNS} | {(settings.confluence_min_score, settings.alert_cooldown_seconds)}, key=lambda combo: combo[1], ), ) if __name__ == "__main__": asyncio.run(main())