#!/usr/bin/env python3
"""
Cheap intraday mechanical poller for the high-risk options sleeve (added
2026-08-31). No web searches, no Claude invocation, no MCP tools -- just
tmx_quotes.get_quote() against sleeve_watchlist.json's static candidate list,
run every 5 minutes during TSX hours from sleeve_monitor.sh. This mirrors
check_watchlist_triggers.py's role: a cheap mechanical pre-filter that only
escalates to a real (expensive) Claude-modeled check when something actually
looks like it's happening, instead of running the LLM reconfirm on a timer
regardless of whether anything changed.

Flags a ticker when BOTH:
  1. today's tmx_quotes changePct >= thresholds.momentum_threshold_pct
  2. changePct has grown by >= thresholds.acceleration_threshold_pct over the
     last thresholds.acceleration_lookback_polls polls (recorded in
     sleeve_monitor_state.json) -- i.e. genuinely accelerating right now, not
     a stock that gapped up at the open and has been flat since.

This is a PRICE-ONLY proxy for setup_D_momentum's volume-acceleration
criterion -- tmx_quotes.py's quote has no volume field, so this mechanical
layer can't check real volume. sleeve_reconfirm_prompt.md pulls the real
volume/RSI/MACD via Questrade before any alert claims a genuine setup; this
script's job is only to decide when that more expensive check is worth
running.

De-dupes per ticker per calendar day (same pattern as
watchlist_alerts_state.json) -- fires once per ticker per day, not once per
5-min poll for the duration of a single move.

Prints one line per newly-triggered ticker to stdout (ticker + changePct),
consumed by sleeve_monitor.sh the same way check_positions.sh consumes
check_watchlist_triggers.py's stdout.

Run manually: python3 sleeve_monitor.py
Cron: sleeve_monitor.sh, every 5 min during TSX hours -- see crontab.
"""
import json
import sys
from datetime import date, datetime
from pathlib import Path

BASE = Path(__file__).parent
sys.path.insert(0, str(BASE))
from is_trading_day import is_trading_day
from tmx_quotes import get_quote

if not is_trading_day():
    sys.exit(0)

watchlist_file = BASE / 'sleeve_watchlist.json'
state_file = BASE / 'sleeve_monitor_state.json'

with open(watchlist_file) as f:
    config = json.load(f)

tickers = config['tickers']
th = config['thresholds']
momentum_threshold = th['momentum_threshold_pct']
lookback = th['acceleration_lookback_polls']
accel_threshold = th['acceleration_threshold_pct']

today = date.today().isoformat()

state = {}
if state_file.exists():
    with open(state_file) as f:
        state = json.load(f)

# Drop history from a prior day so acceleration is never computed across a
# session boundary (an overnight gap isn't intraday acceleration).
if state.get('date') != today:
    state = {'date': today, 'history': {}, 'alerted': {}}

history = state.setdefault('history', {})
alerted = state.setdefault('alerted', {})
prices = state.setdefault('prices', {})

new_alerts = []

for ticker in tickers:
    quote = get_quote(ticker)
    if not quote or quote.get('changePct') is None:
        continue
    chg = quote['changePct']
    prices[ticker] = quote['price']

    hist = history.setdefault(ticker, [])
    hist.append(chg)
    if len(hist) > lookback + 1:
        hist[:] = hist[-(lookback + 1):]

    if alerted.get(ticker):
        continue

    if chg < momentum_threshold:
        continue

    if len(hist) <= lookback:
        # Not enough polls yet today to judge acceleration -- wait for more history.
        continue

    accel = chg - hist[0]
    if accel < accel_threshold:
        continue

    alerted[ticker] = True
    new_alerts.append(f"{ticker.replace('.TO', '')} SLEEVE CANDIDATE: {chg:+.2f}% today, "
                       f"+{accel:.2f}% over last {lookback} polls (price ${quote['price']:.2f})")

state['last_updated'] = datetime.now().isoformat(timespec='seconds')

with open(state_file, 'w') as f:
    json.dump(state, f, indent=2)

for msg in new_alerts:
    print(msg)
