#!/usr/bin/env python3
"""
aiden-copy — NinjaTrader connector.

Polls the aiden-copy Worker for new trade signals and executes them LOCALLY in your own
NinjaTrader 8 via the Automated Trading Interface (ATI) — by writing Order Instruction Files
(OIF) into NinjaTrader's `incoming` folder. NinjaTrader picks them up and routes them through
YOUR broker connection. Your broker credentials never leave your machine.

Setup (one time):
  1. NinjaTrader 8 -> Control Center -> Tools -> Options -> General:
     enable "Automated trading interface (ATI)". (NT watches Documents/NinjaTrader 8/incoming.)
  2. pip install requests
  3. Copy config.example.json -> config.json and fill in your token / account / instruments.
  4. python ninja_copy.py

Sim first: point `account` at "Sim101" until you've watched it work.
"""
import json, os, time, sys, pathlib, urllib.request, urllib.error

HERE = pathlib.Path(__file__).resolve().parent
CFG = json.loads((HERE / "config.json").read_text()) if (HERE / "config.json").exists() else {}

WORKER   = CFG.get("worker",   "https://aiden-copy.adamfrankwoodward.workers.dev")
STRATEGY = CFG.get("strategy", "demo")
TOKEN    = CFG.get("token",    "")
ACCOUNT  = CFG.get("account",  "Sim101")           # NinjaTrader account name (overrides server if set)
INTERVAL = float(CFG.get("pollSeconds", 1.5))
TIF      = CFG.get("tif", "DAY")
# map our short symbol -> the exact NinjaTrader instrument string (with contract month)
INSTRUMENTS = CFG.get("instruments", {"MNQ": "MNQ 09-26", "MES": "MES 09-26", "MGC": "MGC 08-26"})
INCOMING = pathlib.Path(os.path.expanduser(CFG.get("incoming",
            str(pathlib.Path.home() / "Documents" / "NinjaTrader 8" / "incoming"))))
CURSOR_FILE = HERE / f".cursor_{STRATEGY}"

def log(*a): print(time.strftime("%H:%M:%S"), *a, flush=True)

def load_cursor():
    try: return int(CURSOR_FILE.read_text().strip())
    except Exception: return 0

def save_cursor(n): CURSOR_FILE.write_text(str(n))

def nt_instrument(sym): return INSTRUMENTS.get(sym.upper(), sym.upper())

def write_oif(line):
    INCOMING.mkdir(parents=True, exist_ok=True)
    # unique filename so NinjaTrader processes each separately
    fname = INCOMING / f"oif_{int(time.time()*1000)}_{os.getpid()}.txt"
    fname.write_text(line + "\n")
    log("OIF ->", line)

def oif_lines(sig):
    """Translate a signal into one or more NinjaTrader ATI OIF command lines.
    ENTER with a stop and/or target emits an entry PLUS an OCO bracket (protective stop +
    take-profit that cancel each other). Returns a list of OIF command strings."""
    acct = ACCOUNT or sig.get("account", "Sim101")
    instr = nt_instrument(sig["symbol"])
    action = sig.get("action", "ENTER").upper()
    qty = int(sig.get("qty", 0))
    if qty <= 0:
        log("skip", sig["symbol"], "qty=0 (governor sized this account to 0 — too big to risk)")
        return []
    if action in ("EXIT", "FLAT", "CLOSE"):
        # close the position; cancel any resting bracket orders for the instrument
        return [f"CLOSEPOSITION;{acct};{instr};;;;;;;;;;"]

    side = sig.get("side", "BUY").upper()           # BUY = long, SELL = short
    nt_action = "BUY" if side == "BUY" else "SELL"
    opp = "SELL" if side == "BUY" else "BUY"        # protective orders are the opposite side
    otype = sig.get("orderType", "MARKET").upper()
    nt_otype = {"MARKET": "MARKET", "LIMIT": "LIMIT", "STOP": "STOPMARKET",
                "STOPMARKET": "STOPMARKET", "STOPLIMIT": "STOPLIMIT"}.get(otype, "MARKET")
    limit = sig.get("price") if nt_otype in ("LIMIT", "STOPLIMIT") else ""
    entry_stop = sig.get("price") if nt_otype in ("STOPMARKET", "STOPLIMIT") else ""
    eid = f"aiden-{sig['id']}"
    # PLACE;ACCOUNT;INSTRUMENT;ACTION;QTY;ORDER TYPE;LIMIT;STOP;TIF;OCO;ORDER ID;STRATEGY;STRATEGY ID
    lines = [f"PLACE;{acct};{instr};{nt_action};{qty};{nt_otype};{limit};{entry_stop};{TIF};;{eid};;"]

    stop_px = sig.get("stop")
    tgt_px = sig.get("target")
    if stop_px is not None or tgt_px is not None:
        oco = f"oco-{sig['id']}"   # shared id links the bracket; one fill cancels the other
        if stop_px is not None:
            lines.append(f"PLACE;{acct};{instr};{opp};{qty};STOPMARKET;;{stop_px};GTC;{oco};{eid}-sl;;")
        if tgt_px is not None:
            lines.append(f"PLACE;{acct};{instr};{opp};{qty};LIMIT;{tgt_px};;GTC;{oco};{eid}-tp;;")
    return lines

def poll():
    since = load_cursor()
    url = f"{WORKER}/poll/{STRATEGY}?token={TOKEN}&since={since}"
    req = urllib.request.Request(url, headers={"User-Agent": "aiden-copy-connector/1.0", "Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as r:
            data = json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        log("poll HTTP", e.code, e.read().decode()[:120]); return
    except Exception as e:
        log("poll error:", e); return
    if data.get("killed"):
        log("** strategy kill-switch is ON — no new entries **")
    for sig in data.get("signals", []):
        for line in oif_lines(sig):
            write_oif(line)
            time.sleep(0.15)  # distinct filenames + ordered pickup (entry before bracket)
        save_cursor(sig["id"])

def main():
    log(f"aiden-copy connector | strategy={STRATEGY} account={ACCOUNT} incoming={INCOMING}")
    if not TOKEN:
        log("ERROR: no subscriber token in config.json"); sys.exit(1)
    while True:
        poll()
        time.sleep(INTERVAL)

if __name__ == "__main__":
    main()
