#!/usr/bin/env python3
"""
Cheap intraday check: compare live_prices.json against each open position's
stop/target levels in positions.json. No web searches, no Claude invocation --
just arithmetic against data fetch_prices.py already refreshes every 5 minutes.

Also checks that every open position actually HAS a stop_loss_cad, past a
grace period -- added 2026-08-01 after finding a position with a null stop
ran unmanaged for 59 days because a null stop makes the stop-hit check below
never fire at all (`if stop is not None and price <= stop`). This is the
same hourly loop, so it doesn't need a live quote to fire -- a missing stop
is knowable from positions.json alone.

Prints one line per NEWLY-triggered condition (stop hit / target hit / no
stop set) to stdout, so the calling shell script can decide whether to fire
a push notification. De-dupes against alerts_state.json so the same
condition doesn't re-fire every run until the position is actually closed,
enriched, or the next tier is hit.

Does NOT check: time stops (day-count, belongs in the once-daily full scan) or
the aggressive-mode pyramid trigger (needs intraday volume + swing-high data
that isn't tracked yet -- see agent_and_tips.md Part 10).

Run manually:  python3 check_positions.py
Cron (hourly during TSX hours, MDT):
  30 7-13 * * 1-5 /media/raid/rshare/SwingTrader/check_positions.sh >> /media/raid/rshare/SwingTrader/daily_run.log 2>&1
"""
import json
import sys
from datetime import date
from pathlib import Path

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

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

positions_file = BASE / 'positions.json'
prices_file = BASE / 'live_prices.json'
state_file = BASE / 'position_alerts_state.json'
criteria_file = BASE / 'screen_criteria.json'

positions = json.load(open(positions_file)).get('open_positions', [])
prices = json.load(open(prices_file)).get('prices', {})
state = json.load(open(state_file)) if state_file.exists() else {}
criteria = json.load(open(criteria_file)) if criteria_file.exists() else {}
grace_days = criteria.get('trade_management', {}).get('unenriched_position_grace_days', 5)

open_tickers = {p['ticker'].upper() for p in positions if p.get('ticker')}
# Drop stale state for positions that are no longer open (closed/exited).
state = {k: v for k, v in state.items() if k.split('|', 1)[0] in open_tickers}

new_alerts = []
today = date.today()

for pos in positions:
    ticker = pos.get('ticker', '').upper()
    if not ticker:
        continue

    def fire(key, message):
        if state.get(f"{ticker}|{key}"):
            return
        state[f"{ticker}|{key}"] = True
        new_alerts.append(message)

    stop = pos.get('stop_loss_cad')
    if stop is None:
        try:
            entry_dt = date.fromisoformat(pos['entry_date']) if pos.get('entry_date') else None
        except ValueError:
            entry_dt = None
        days_held = (today - entry_dt).days if entry_dt else None
        if days_held is None or days_held >= grace_days:
            fire('no_stop', f"{ticker} has NO STOP set after {days_held if days_held is not None else '?'} days -- unmanaged risk, enrich it now")

    quote = prices.get(ticker)
    if not quote or quote.get('price') is None:
        continue
    price = quote['price']
    entry = pos.get('entry_price_cad')
    t1, t2, t3 = pos.get('target_1_cad'), pos.get('target_2_cad'), pos.get('target_3_cad')
    t1_hit, t2_hit = pos.get('target_1_hit', False), pos.get('target_2_hit', False)

    if stop is not None and price <= stop:
        fire('stop', f"{ticker} STOP HIT ${price:.2f} (stop ${stop:.2f}, entry ${entry:.2f}) -- exit now")
    elif not t1_hit and t1 is not None and price >= t1:
        fire('t1', f"{ticker} T1 HIT ${price:.2f} (target ${t1:.2f}) -- sell 20%, stop to breakeven")
    elif t1_hit and not t2_hit and t2 is not None and price >= t2:
        fire('t2', f"{ticker} T2 HIT ${price:.2f} (target ${t2:.2f}) -- sell 30%, trail stop below 20 EMA")
    elif t2_hit and t3 is not None and price >= t3:
        fire('t3', f"{ticker} T3 HIT ${price:.2f} (target ${t3:.2f}) -- runner target reached")

json.dump(state, open(state_file, 'w'), indent=2)

for msg in new_alerts:
    print(msg)
