"""FlashAlpha -> Interactive Brokers reference options executor.

Companion download for:
https://flashalpha.com/articles/flashalpha-interactive-brokers-ib-insync-automated-options-execution

PAPER FIRST (TWS/Gateway paper port 7497). Educational reference code, not
investment advice. Replace decide() with YOUR validated rule before trusting
it with anything - see the article for the full walkthrough of every guard.

Requires:  pip install flashalpha ib_async
"""
import time
from dataclasses import dataclass, field
from datetime import date, datetime

from flashalpha import FlashAlpha
from ib_async import IB, Option, Stock

API_KEY = "YOUR_FLASHALPHA_KEY"
SYMBOL, POLL_S = "SPY", 60
MAX_OPEN, DAILY_LOSS_CAP = 1, -500.0     # concurrent entry order groups, USD
SCORE_MIN = 80


@dataclass
class State:
    open_orders: int = 0
    day: date = field(default_factory=date.today)
    day_pnl: float = 0.0
    killed: bool = False


def decide(fa: FlashAlpha) -> str | None:
    """Return 'C' (buy call) / 'P' (buy put) / None. Replace with YOUR validated rule."""
    gex = fa.gex(SYMBOL)
    sigs = fa.flow_signals(SYMBOL)
    hc = [s["signal"] for s in sigs.get("signals", [])
          if s["signal"]["score"] >= SCORE_MIN and "opening" in s["signal"].get("tags", [])]
    if gex.get("net_gex", 0) < 0:          # short-gamma regime: moves extend
        if any(s["intent"] == 1 for s in hc):
            return "C"
        if any(s["intent"] == 2 for s in hc):
            return "P"
    return None


def pick_contract(ib: IB, right: str, spot: float):
    """Nearest weekly expiry >= 7 DTE, strike nearest spot, SMART-routed."""
    stock = ib.qualifyContracts(Stock(SYMBOL, "SMART", "USD"))[0]
    chains = ib.reqSecDefOptParams(SYMBOL, "", "STK", stock.conId)
    chain = next(c for c in chains if c.exchange == "SMART")
    expiry = sorted(e for e in chain.expirations
                    if (datetime.strptime(e, "%Y%m%d").date() - date.today()).days >= 7)[0]
    strike = min(chain.strikes, key=lambda k: abs(k - spot))
    opt = Option(SYMBOL, expiry, strike, right, "SMART", currency="USD")
    ib.qualifyContracts(opt)
    return opt


def place_bracket(ib: IB, opt, qty: int):
    [tkr] = ib.reqTickers(opt)
    mid = (tkr.bid + tkr.ask) / 2 if tkr.bid and tkr.ask else tkr.marketPrice()
    entry = round(mid, 2)                              # alpha=0: improve to taste
    bracket = ib.bracketOrder("BUY", qty, limitPrice=entry,
                              takeProfitPrice=round(entry * 1.5, 2),
                              stopLossPrice=round(entry * 0.5, 2))
    for o in bracket:
        ib.placeOrder(opt, o)
    return bracket


def main():
    fa = FlashAlpha(api_key=API_KEY)
    ib = IB()
    ib.connect("127.0.0.1", 7497, clientId=17)         # PAPER port
    st = State()

    def on_exec(trade, fill):
        print(f"FILL {fill.contract.localSymbol} {fill.execution.shares} @ {fill.execution.price}")
    ib.execDetailsEvent += on_exec

    def on_commission(trade, fill, report):
        pnl = report.realizedPNL
        if pnl and abs(pnl) < 1e300:       # IB's unset sentinel is DBL_MAX - filter it
            st.day_pnl += pnl
    ib.commissionReportEvent += on_commission

    while True:
        try:
            if date.today() != st.day:                 # new session: reset caps
                st = State()
            if st.day_pnl <= DAILY_LOSS_CAP and not st.killed:
                st.killed = True
                for t in ib.openTrades():
                    if t.order.parentId == 0:      # cancel ENTRY orders only -
                        ib.cancelOrder(t.order)    # bracket TP/stops stay working
                print("KILL SWITCH: daily loss cap hit; entries cancelled, going flat-only")
            open_parents = sum(1 for t in ib.openTrades() if t.order.parentId == 0)
            if not st.killed and open_parents < MAX_OPEN:
                right = decide(fa)
                if right:
                    spot = fa.gex(SYMBOL)["underlying_price"]
                    opt = pick_contract(ib, right, spot)
                    place_bracket(ib, opt, qty=1)
                    print(f"ORDER {opt.localSymbol} bracket placed")
        except ConnectionError:
            print("IBKR disconnected; retrying in 30s")
            ib.disconnect()
            time.sleep(30)
            ib.connect("127.0.0.1", 7497, clientId=17)
        except Exception as e:
            print("loop error (continuing):", e)
        ib.sleep(POLL_S)                                # keeps the event loop serviced


if __name__ == "__main__":
    main()
