diff --git a/infrastructure/systemd/ftdt-collector.service b/infrastructure/systemd/ftdt-collector.service new file mode 100644 index 0000000..e977e2a --- /dev/null +++ b/infrastructure/systemd/ftdt-collector.service @@ -0,0 +1,26 @@ +# FTDT Quant Lab — Data Collector +# Install: sudo cp ftdt-collector.service /etc/systemd/system/ +# sudo systemctl enable ftdt-collector +# sudo systemctl start ftdt-collector + +[Unit] +Description=FTDT Quant Lab — Hyperliquid Data Collector +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=satoshi +WorkingDirectory=/home/satoshi/ftdt-quant-lab +Environment=PYTHONUNBUFFERED=1 +Environment=PATH=/home/satoshi/ftdt-quant-lab/.venv/bin:/usr/local/bin:/usr/bin:/bin +ExecStart=/home/satoshi/ftdt-quant-lab/.venv/bin/python -m cli collect --mainnet --coins BTC,ETH,SOL,HYPE --data-dir data/raw --poll-interval 60 --flush-interval 5 +Restart=always +RestartSec=10 +StandardOutput=append:/home/satoshi/ftdt-quant-lab/logs/collector.log +StandardError=append:/home/satoshi/ftdt-quant-lab/logs/collector.log +MemoryMax=512M +CPUQuota=50% + +[Install] +WantedBy=multi-user.target diff --git a/infrastructure/systemd/ftdt-dashboard.service b/infrastructure/systemd/ftdt-dashboard.service new file mode 100644 index 0000000..6bc2b7f --- /dev/null +++ b/infrastructure/systemd/ftdt-dashboard.service @@ -0,0 +1,26 @@ +# FTDT Quant Lab — Dashboard Server +# Install: sudo cp ftdt-dashboard.service /etc/systemd/system/ +# sudo systemctl enable ftdt-dashboard +# sudo systemctl start ftdt-dashboard + +[Unit] +Description=FTDT Quant Lab — FastAPI Dashboard Backend +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=satoshi +WorkingDirectory=/home/satoshi/ftdt-quant-lab +Environment=PYTHONUNBUFFERED=1 +Environment=PATH=/home/satoshi/ftdt-quant-lab/.venv/bin:/usr/local/bin:/usr/bin:/bin +ExecStart=/home/satoshi/ftdt-quant-lab/.venv/bin/python dashboard/server.py --port 9175 +Restart=always +RestartSec=5 +StandardOutput=append:/home/satoshi/ftdt-quant-lab/logs/dashboard.log +StandardError=append:/home/satoshi/ftdt-quant-lab/logs/dashboard.log +MemoryMax=256M +CPUQuota=25% + +[Install] +WantedBy=multi-user.target diff --git a/infrastructure/systemd/ftdt-paper.service b/infrastructure/systemd/ftdt-paper.service new file mode 100644 index 0000000..82529d7 --- /dev/null +++ b/infrastructure/systemd/ftdt-paper.service @@ -0,0 +1,27 @@ +# FTDT Quant Lab — Paper Trading Node +# Install: sudo cp ftdt-paper.service /etc/systemd/system/ +# sudo systemctl enable ftdt-paper +# sudo systemctl start ftdt-paper + +[Unit] +Description=FTDT Quant Lab — Paper Trading Node (v2) +After=network-online.target ftdt-collector.service +Wants=network-online.target +Requires=ftdt-collector.service + +[Service] +Type=simple +User=satoshi +WorkingDirectory=/home/satoshi/ftdt-quant-lab +Environment=PYTHONUNBUFFERED=1 +Environment=PATH=/home/satoshi/ftdt-quant-lab/.venv/bin:/usr/local/bin:/usr/bin:/bin +ExecStart=/home/satoshi/ftdt-quant-lab/.venv/bin/python -m live.node_v2 --testnet --coins BTC,ETH --mode paper --equity 100000 --metrics-file /tmp/ftdt-metrics-v2.json +Restart=always +RestartSec=15 +StandardOutput=append:/home/satoshi/ftdt-quant-lab/logs/paper-node.log +StandardError=append:/home/satoshi/ftdt-quant-lab/logs/paper-node.log +MemoryMax=512M +CPUQuota=50% + +[Install] +WantedBy=multi-user.target diff --git a/live/paper_trader.py b/live/paper_trader.py index 547105e..dc44c5a 100644 --- a/live/paper_trader.py +++ b/live/paper_trader.py @@ -20,6 +20,9 @@ from strategies.deep_lob import DeepLOB from strategies.cartea_jaimungal import CarteaJaimungal from strategies.queue_imbalance import QueueImbalance from strategies.gueant import GueantMM +from strategies.wqi_predictor import WQIPredictor +from strategies.funding_arb_strategy import FundingArb +from sim.fills import QueueAwareFillModel logging.basicConfig(level=logging.INFO, format="%(asctime)s [paper] %(message)s", datefmt="%H:%M:%S") log = logging.getLogger("ftdt-paper") @@ -129,6 +132,20 @@ STRATEGIES = { "signals": [], "type": "gueant", "size": 0.001, "fee_model": "maker", "description": "Closed-form market making — Guéant-Lehalle asymptotic solution. Handles asymmetric information with adverse-selection-adjusted spreads. Computationally efficient closed form.", }, + "WQI Predictor": { + "allocation": 10000.0, "instrument": "BTC", "pnl": 0.0, + "trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle", + "position": 0.0, "entry_price": 0.0, "fee_paid": 0.0, + "signals": [], "type": "wqi", "size": 0.002, "fee_model": "taker", + "description": "Weighted Queue Imbalance directional predictor — enters on extreme WQI z-score with adverse selection gating. Exits on timeout, reversal, or stop-loss.", + }, + "Funding Rate Arb": { + "allocation": 10000.0, "instrument": "BTC", "pnl": 0.0, + "trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle", + "position": 0.0, "entry_price": 0.0, "fee_paid": 0.0, + "signals": [], "type": "funding_arb", "size": 0.005, "fee_model": "taker", + "description": "Delta-neutral funding rate carry — shorts perp when funding APR is extreme, collects hourly payments. Exits when rate fades, flips, or max hold reached.", + }, } trades_log: list[dict] = [] @@ -226,6 +243,13 @@ queue_imb = QueueImbalance(depth_levels=10) gueant = GueantMM(gamma=0.1, sigma=0.015, k=1.5, T=3600, max_pos=0.005) prev_bids = None prev_asks = None +fill_model = QueueAwareFillModel() +wqi_predictors = {coin: WQIPredictor(z_entry=2.0, max_hold_seconds=30, + stop_loss_bps=5.0, take_profit_bps=10.0, + size=0.001, fee_model="taker") + for coin in ("BTC", "ETH")} +funding_arb = FundingArb(apr_threshold=0.30, apr_exit=0.10, size=0.001, + max_hold_hours=48.0, taker_fee_pct=TAKER_FEE) # ═══════════════════════ Signal Engine ═══════════════════════ @@ -433,13 +457,10 @@ def simulate_fill(name: str, side: str, coin: str, price: float, reason: str = " # ═══════════════════════ A-S Spread Capture ═══════════════════════ -def simulate_avellaneda(btc_bid, btc_ask): - """Avellaneda-Stoikov: regime-adaptive spread capture. - - Regime-dependent behavior: - LOW_VOL → fill_prob=25%, tight margins (capture small spreads frequently) - NORMAL → fill_prob=15%, baseline - HIGH_VOL → fill_prob=8%, skip if spread too wide (adverse selection risk) +def simulate_avellaneda(btc_bid, btc_ask, bid_depth=2.0, ask_depth=2.0): + """Avellaneda-Stoikov: queue-aware spread capture with regime gating. + + Uses QueueAwareFillModel instead of random probabilities. """ cfg = STRATEGIES["Avellaneda-Stoikov"] if btc_bid <= 0 or btc_ask <= 0: @@ -448,57 +469,68 @@ def simulate_avellaneda(btc_bid, btc_ask): regime = current_regime spread = btc_ask - btc_bid - # Regime-dependent fill probability + if regime == "HIGH_VOL" and spread > 30: + return + + mid = (btc_bid + btc_ask) / 2 + sz = cfg["size"] + quote_bid = btc_bid + quote_ask = btc_ask + if regime == "LOW_VOL": - fill_prob = 0.25 + quote_bid = btc_bid + spread * 0.05 + quote_ask = btc_ask - spread * 0.05 elif regime == "HIGH_VOL": - fill_prob = 0.08 - # During high vol with wide spreads, avoid getting picked off - if spread > 30: # >$30 spread = dangerous - return - else: - fill_prob = 0.15 + quote_bid = btc_bid - spread * 0.1 + quote_ask = btc_ask + spread * 0.1 - if random.random() < fill_prob: - if cfg["position"] <= 0: - bid_fill_price = btc_bid - else: - bid_fill_price = btc_ask - - side = "BUY" if cfg["position"] <= 0 else "SELL" - sz = cfg["size"] - notional = sz * bid_fill_price - fee = notional * MAKER_FEE # A-S is a MAKER strategy — pay maker fee, not taker - spread_profit = sz * (btc_ask - btc_bid)/2 if side == "BUY" else 0 - - if side == "BUY": - if cfg["position"] < 0: - close_pnl = abs(cfg["position"]) * (cfg["entry_price"] - bid_fill_price) - cfg["pnl"] += close_pnl - if close_pnl > 0: cfg["wins"] += 1 - cfg["entry_price"] = bid_fill_price - cfg["position"] = sz - cfg["pnl"] += spread_profit - fee - else: - if cfg["position"] > 0: - close_pnl = cfg["position"] * (bid_fill_price - cfg["entry_price"]) - cfg["pnl"] += close_pnl - if close_pnl > 0: cfg["wins"] += 1 - trades_log.append({ - "time": datetime.now().strftime("%H:%M:%S"), - "strategy": "Avellaneda-Stoikov", - "side": "SELL", "size": sz, - "price": bid_fill_price, - "pnl": round(close_pnl - fee, 4), - "fee": round(fee, 4), - }) - cfg["position"] = 0 - cfg["entry_price"] = 0 + depth = max(bid_depth, ask_depth, 1.0) + depth_ahead = depth * 0.5 + bid_fill = fill_model.check_fill( + aggressor_side="sell", agg_size=depth * 0.3, agg_price=max(quote_bid, 1), + our_price=quote_bid, our_size=sz, depth_ahead=depth_ahead, + ) + if bid_fill["filled"] and cfg["position"] <= 0: + fee = sz * bid_fill["fill_size"] * quote_bid * MAKER_FEE cfg["fee_paid"] += fee + spread_profit = bid_fill["fill_size"] * (btc_ask - quote_bid) / 2 + if cfg["position"] < 0: + close_pnl = abs(cfg["position"]) * (cfg["entry_price"] - quote_bid) + cfg["pnl"] += close_pnl + if close_pnl > 0: + cfg["wins"] += 1 + cfg["entry_price"] = quote_bid + cfg["position"] = sz + cfg["pnl"] += spread_profit - fee cfg["trades_today"] += 1 cfg["pnl_pct"] = cfg["pnl"] / cfg["allocation"] * 100 - strategy_equity["Avellaneda-Stoikov"].append({"t": time.time(), "v": cfg["allocation"] + cfg["pnl"]}) + strategy_equity["Avellaneda-Stoikov"].append( + {"t": time.time(), "v": cfg["allocation"] + cfg["pnl"]} + ) + return + + ask_fill = fill_model.check_fill( + aggressor_side="buy", agg_size=depth * 0.3, agg_price=min(quote_ask, mid * 2), + our_price=quote_ask, our_size=sz, depth_ahead=depth_ahead, + ) + if ask_fill["filled"] and cfg["position"] >= 0: + fee = sz * ask_fill["fill_size"] * quote_ask * MAKER_FEE + cfg["fee_paid"] += fee + spread_profit = ask_fill["fill_size"] * (quote_ask - btc_bid) / 2 + if cfg["position"] > 0: + close_pnl = cfg["position"] * (quote_ask - cfg["entry_price"]) + cfg["pnl"] += close_pnl + if close_pnl > 0: + cfg["wins"] += 1 + cfg["entry_price"] = quote_ask + cfg["position"] = -sz + cfg["pnl"] += spread_profit - fee + cfg["trades_today"] += 1 + cfg["pnl_pct"] = cfg["pnl"] / cfg["allocation"] * 100 + strategy_equity["Avellaneda-Stoikov"].append( + {"t": time.time(), "v": cfg["allocation"] + cfg["pnl"]} + ) # ═══════════════════════ Metrics ═══════════════════════ @@ -522,6 +554,13 @@ def write_metrics(): "strategy_equity": {k: list(v)[-300:] for k, v in strategy_equity.items()}, "strategies": STRATEGIES, "trades": trades_log[-200:], + "wqi": {c: wqi_predictors[c].summary() for c in wqi_predictors}, + "funding_arb": funding_arb.summary(), + "fill_model": { + "fill_rate": round(fill_model.fill_rate(), 4), + "fills": fill_model.fill_count, + "skips": fill_model.skip_count, + }, "status": "running", "btc_price": btc_prices[-1] if btc_prices else 0, "eth_price": eth_prices[-1] if eth_prices else 0, @@ -590,8 +629,10 @@ async def main(): btc_bid, btc_ask = get_mainnet_orderbook("BTC") bids, asks = get_deep_orderbook("BTC") - # Avellaneda-Stoikov: simulate spread capture - simulate_avellaneda(btc_bid, btc_ask) + # Avellaneda-Stoikov: simulate spread capture with queue-aware fills + bid_depth = sum(sz for _, sz in bids[:10]) if bids else 2.0 + ask_depth = sum(sz for _, sz in asks[:10]) if asks else 2.0 + simulate_avellaneda(btc_bid, btc_ask, bid_depth, ask_depth) # Hawkes OFI: feed simulated trade to model hawkes_btc.update("B" if tick % 2 == 0 else "S", 0.001, btc) @@ -660,7 +701,6 @@ async def main(): g_quotes = gueant.optimal_quotes( btc, gueant_inv, tick % 3600, adverse_prob=queue_imb.wqi_history[-1] if queue_imb.wqi_history else 0) - # Simulate fill: if our quote is at/near best, track a signal if btc_bid > 0 and g_quotes["bid"] >= btc_bid * 0.999: STRATEGIES["Guéant Market Making"]["signals"].append({ "time": time.time(), "signal": "BUY", @@ -672,7 +712,39 @@ async def main(): "strength": 0.5, }) - # Process next strategy's signals (round-robin 9 strategies) + # WQI Predictor: directional signal from queue imbalance + if bids and asks and btc > 0: + try: + wqi_signal = wqi_predictors.get("BTC") + if wqi_signal: + sig = wqi_signal.feed_signal(bids, asks, btc, + prev_bids, prev_asks, + btc_prices[-2] if len(btc_prices) >= 2 else 0) + if sig["action"] in ("BUY", "SELL"): + STRATEGIES["WQI Predictor"]["signals"].append({ + "time": time.time(), + "signal": sig["action"], + "strength": abs(sig["z_score"]) / 3.0, + "reason": f"z={sig['z_score']:.2f}_wqi={sig['wqi']:.3f}", + }) + except Exception: + pass + + # Funding Rate Arb: check funding signal + if funding_rates and isinstance(funding_rates[-1], dict): + fr = funding_rates[-1].get("BTC", 0) + if fr != 0: + annual_apr = abs(fr) * 1095 + arb_signal = funding_arb.signal(annual_apr, btc) + if arb_signal["action"] != "HOLD": + STRATEGIES["Funding Rate Arb"]["signals"].append({ + "time": time.time(), + "signal": arb_signal["action"], + "strength": min(1.0, annual_apr), + "reason": arb_signal.get("reason", ""), + }) + + # Process next strategy's signals (round-robin all strategies) total_strats = len(strategy_names) name = strategy_names[idx % total_strats] idx += 1 diff --git a/scripts/kill_switch.py b/scripts/kill_switch.py new file mode 100644 index 0000000..ae72f02 --- /dev/null +++ b/scripts/kill_switch.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +""" +FTDT Quant Lab — Emergency Kill Switch. + +Flattens all active positions and cancels all open orders. +Runs against either testnet or mainnet Hyperliquid. + +Usage: + python scripts/kill_switch.py --mainnet # Production emergency stop + python scripts/kill_switch.py --testnet # Testnet safety test + python scripts/kill_switch.py --testnet --dry-run # Print what would happen + +The script: + 1. Loads private key from environment (HL_PRIVATE_KEY) or keyfile + 2. Fetches current positions from HL API + 3. Issues market/close orders to flatten each position + 4. Cancels all open orders + 5. Verifies positions are zero +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +MAINNET_API = "https://api.hyperliquid.xyz/info" +TESTNET_EXCHANGE = "https://api.hyperliquid-testnet.xyz/exchange" +MAINNET_EXCHANGE = "https://api.hyperliquid.xyz/exchange" + + +def get_private_key() -> str: + pk = os.environ.get("HL_PRIVATE_KEY", "") + if pk: + return pk + + keyfile = Path.home() / ".hl" / "key" + if keyfile.exists(): + return keyfile.read_text().strip() + + return "" + + +def sign_l1_action(action: dict, private_key: str) -> dict: + try: + from eth_account import Account + import eth_account.messages + except ImportError: + print("eth_account not installed. Install with: pip install eth-account") + sys.exit(1) + + account = Account.from_key(private_key) + from datetime import datetime, timezone + + typed_data = { + "domain": {"chainId": 1337}, + "types": { + "HyperliquidTransaction": [ + {"name": "txType", "type": "string"}, + {"name": "nonce", "type": "uint64"}, + ], + }, + "primaryType": "HyperliquidTransaction", + "message": { + "txType": action.get("type", "order"), + "nonce": int(time.time() * 1000), + }, + } + + signable = eth_account.messages.encode_typed_data( + typed_data["domain"], + typed_data["types"], + typed_data["message"], + ) + sig = account.sign_message(signable) + action["signature"] = sig.signature.hex() + return action + + +def fetch_positions(api_url: str) -> list[dict]: + import requests + pk = get_private_key() + if not pk: + return [] + + try: + from eth_account import Account + account = Account.from_key(pk) + resp = requests.post( + api_url, + json={"type": "clearinghouseState", "user": account.address}, + timeout=10, + ) + data = resp.json() + positions = [] + if isinstance(data, dict): + asset_positions = data.get("assetPositions", []) + for pos in asset_positions: + p = pos.get("position", {}) + coin = p.get("coin", "?") + szi = float(p.get("szi", 0)) + if szi != 0: + positions.append({ + "coin": coin, + "szi": szi, + "entry_px": float(p.get("entryPx", 0)), + "leverage": p.get("leverage", {}), + }) + return positions + except Exception as e: + print(f"Failed to fetch positions: {e}") + return [] + + +def close_position(exchange_url: str, coin: str, current_szi: float, dry_run: bool) -> bool: + import requests + + side = "A" if current_szi < 0 else "B" + close_size = abs(current_szi) + + action = { + "type": "order", + "orders": [{ + "a": 0, + "b": open_price(exchange_url.replace("exchange", "info"), coin, side), + "p": open_price(exchange_url.replace("exchange", "info"), coin, side), + "s": round(close_size, 6), + "r": False, + "t": {"limit": {"tif": "Ioc"}}, + }], + "grouping": "na", + } + + if not dry_run: + pk = get_private_key() + if pk: + action = sign_l1_action(action, pk) + try: + resp = requests.post(exchange_url, json=action, timeout=10) + result = resp.json() + if resp.status_code == 200: + print(f" [{coin}] Closed {close_size:.4f} ({side}) — OK") + return True + else: + print(f" [{coin}] Close failed: {result}") + return False + except Exception as e: + print(f" [{coin}] Close error: {e}") + return False + else: + print(f" [{coin}] [DRY RUN] Would close {close_size:.4f} ({side})") + return True + + +def open_price(api_url: str, coin: str, side: str) -> float: + import requests + try: + resp = requests.post(api_url, json={"type": "l2Book", "coin": coin}, timeout=5) + data = resp.json() + levels = data.get("levels", []) + if levels and len(levels) >= 2: + bids = levels[0] + asks = levels[1] + if side == "A": + px = float(bids[0]["px"]) if bids else 0 + return round(px * 0.99, 1) + else: + px = float(asks[0]["px"]) if asks else 0 + return round(px * 1.01, 1) + except Exception: + pass + return 0.0 + + +def cancel_all_orders(exchange_url: str, api_url: str, dry_run: bool): + import requests + pk = get_private_key() + if not pk: + print(" No private key — cannot cancel orders") + return + + from eth_account import Account + account = Account.from_key(pk) + + try: + resp = requests.post( + api_url, + json={"type": "openOrders", "user": account.address}, + timeout=10, + ) + orders = resp.json() + if not isinstance(orders, list) or len(orders) == 0: + print(" No open orders found") + return + + print(f" Found {len(orders)} open orders") + + if not dry_run: + cancels = [] + for order in orders: + cancels.append({ + "a": order.get("oid", 0), + "b": order.get("coin", "BTC"), + }) + + action = { + "type": "cancel", + "cancels": cancels, + } + action = sign_l1_action(action, pk) + + try: + resp = requests.post(exchange_url, json=action, timeout=10) + if resp.status_code == 200: + print(f" Cancelled {len(cancels)} orders — OK") + else: + print(f" Cancel failed: {resp.json()}") + except Exception as e: + print(f" Cancel error: {e}") + else: + print(f" [DRY RUN] Would cancel {len(orders)} orders") + except Exception as e: + print(f" Failed to fetch orders: {e}") + + +def verify_flat(api_url: str) -> bool: + positions = fetch_positions(api_url) + if not positions: + print(" No positions — flat") + return True + + total = sum(abs(p["szi"]) for p in positions) + if total < 1e-6: + print(" No positions — flat") + return True + + print(f" WARNING: {len(positions)} positions remain (total exposure: {total:.4f})") + for p in positions: + print(f" {p['coin']}: {p['szi']:.4f} @ ${p['entry_px']:.1f}") + return False + + +def main(): + p = argparse.ArgumentParser(description="FTDT Quant Lab — Emergency Kill Switch") + p.add_argument("--testnet", action="store_true", default=True) + p.add_argument("--mainnet", dest="testnet", action="store_false") + p.add_argument("--dry-run", action="store_true", help="Print actions without executing") + p.add_argument("--skip-cancel", action="store_true", help="Skip order cancellation") + p.add_argument("--retry", type=int, default=3, help="Retry count for position closes") + args = p.parse_args() + + api_url = MAINNET_API if not args.testnet else TESTNET_API + exchange_url = MAINNET_EXCHANGE if not args.testnet else TESTNET_EXCHANGE + env_name = "MAINNET" if not args.testnet else "TESTNET" + + print("=" * 60) + print(f" FTDT Quant Lab — KILL SWITCH [{env_name}]") + print("=" * 60) + if args.dry_run: + print(" *** DRY RUN — no orders will be placed ***") + print() + + pk = get_private_key() + if not pk: + print(" ERROR: No private key found.") + print(" Set HL_PRIVATE_KEY environment variable or create ~/.hl/key") + if not args.dry_run: + sys.exit(1) + + print(f" [1] Fetching positions...") + positions = fetch_positions(api_url) + if not positions: + print(" No positions to close") + else: + total_size = sum(abs(p["szi"]) for p in positions) + print(f" Found {len(positions)} position(s) (total size: {total_size:.4f})") + for p in positions: + direction = "LONG" if p["szi"] > 0 else "SHORT" + print(f" {p['coin']}: {direction} {abs(p['szi']):.4f} @ ${p['entry_px']:.1f}") + + print(f"\n [2] Closing positions...") + all_closed = True + for p in positions: + for attempt in range(args.retry): + ok = close_position(exchange_url, p["coin"], p["szi"], args.dry_run) + if ok: + break + if attempt < args.retry - 1: + time.sleep(1) + else: + print(f" [{p['coin']}] Failed after {args.retry} attempts") + all_closed = False + + if not all_closed and not args.dry_run: + print("\n WARNING: Some positions could not be closed!") + else: + print(" All positions closed") + + if not args.skip_cancel: + print(f"\n [3] Cancelling open orders...") + cancel_all_orders(exchange_url, api_url, args.dry_run) + + print(f"\n [4] Verification...") + if not args.dry_run: + time.sleep(2) + ok = verify_flat(api_url) + if ok: + print("\n KILL SWITCH COMPLETE — positions flat, orders cancelled") + else: + print("\n KILL SWITCH WARNING — positions may still exist!") + + +if __name__ == "__main__": + main()