From 5b0e14f1506a419b36ae4a01ea7583aa41215b0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:10:17 +0000 Subject: [PATCH 1/4] Initial plan From bf95369d4a8325da19aa587b9338543a7d4fc051 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:16:25 +0000 Subject: [PATCH 2/4] Implement full ALICE/DECISION PARLIAMENT IBKR broker adapter Agent-Logs-Url: https://github.com/Netrade1/Institutional-Microstructure-/sessions/0f9d3c6e-b4ef-4aa1-88d8-7532473ed78d Co-authored-by: Netrade1 <146481409+Netrade1@users.noreply.github.com> --- __pycache__/alice_broker.cpython-312.pyc | Bin 0 -> 85266 bytes alice_broker.py | 1951 ++++++++++++++++++++++ 2 files changed, 1951 insertions(+) create mode 100644 __pycache__/alice_broker.cpython-312.pyc create mode 100644 alice_broker.py diff --git a/__pycache__/alice_broker.cpython-312.pyc b/__pycache__/alice_broker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cfcd9d93c7f40ba41efe04355b9dbfa10c6c438 GIT binary patch literal 85266 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

L&+(7 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 NFOXYoJzSOke*tzHWIg}@ literal 0 HcmV?d00001 diff --git a/alice_broker.py b/alice_broker.py new file mode 100644 index 0000000..361dbe6 --- /dev/null +++ b/alice_broker.py @@ -0,0 +1,1951 @@ +from __future__ import annotations + +""" +ALICE / DECISION PARLIAMENT +Institutional-Grade IBKR Broker Adapter +--------------------------------------- +Enhanced single-file implementation for integrating a Python trading stack +with Interactive Brokers while preserving a broker-agnostic execution schema. + +Major upgrades in this version +- Stronger validation and safer intent handling +- Better risk checks: cash, notional, confidence, shorting, stop-based order risk +- Paper broker with realized/unrealized PnL tracking +- Bracket-style protective logic in paper mode (stop / take-profit) +- Native IBKR bracket order transmission using parent/child orders +- Open order registry and cancel support +- Price update loop for paper mode to simulate fills and exits +- Broker health checks and cleaner event payloads +- SQLite WAL mode and richer persistence +- More robust IBKR status handling scaffolding + +This is still a scaffold, not a profitability machine. +Always test in IBKR paper trading before enabling live routing. +""" + +import abc +import dataclasses +import datetime as dt +import enum +import json +import logging +import os +import sqlite3 +import threading +import time +import uuid +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union + +try: + from ib_insync import ( + IB, + Stock, + Forex, + Future, + Crypto, + MarketOrder, + LimitOrder, + StopOrder, + StopLimitOrder, + ) + IB_INSYNC_AVAILABLE = True +except Exception: + IB_INSYNC_AVAILABLE = False + IB = object # type: ignore + Stock = Forex = Future = Crypto = object # type: ignore + MarketOrder = LimitOrder = StopOrder = StopLimitOrder = object # type: ignore + + +# ============================================================================= +# LOGGING +# ============================================================================= + +def build_logger(name: str = "alice_broker") -> logging.Logger: + logger = logging.getLogger(name) + if logger.handlers: + return logger + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")) + logger.addHandler(handler) + return logger + + +logger = build_logger() + + +# ============================================================================= +# ENUMS / SCHEMAS +# ============================================================================= + +class Environment(str, enum.Enum): + PAPER = "paper" + LIVE = "live" + + +class AssetType(str, enum.Enum): + STOCK = "stock" + FOREX = "forex" + FUTURE = "future" + CRYPTO = "crypto" + OPTION = "option" + + +class OrderSide(str, enum.Enum): + BUY = "BUY" + SELL = "SELL" + + +class OrderType(str, enum.Enum): + MARKET = "MKT" + LIMIT = "LMT" + STOP = "STP" + STOP_LIMIT = "STP_LMT" + + +class TimeInForce(str, enum.Enum): + DAY = "DAY" + GTC = "GTC" + IOC = "IOC" + FOK = "FOK" + + +class IntentSource(str, enum.Enum): + ALICE = "alice" + MAD_HATTER = "mad_hatter" + PARLIAMENT = "parliament" + HUMAN = "human" + BACKTEST = "backtest" + SCANNER = "scanner" + + +class DecisionAction(str, enum.Enum): + ENTER_LONG = "enter_long" + ENTER_SHORT = "enter_short" + EXIT_LONG = "exit_long" + EXIT_SHORT = "exit_short" + REDUCE = "reduce" + HOLD = "hold" + CANCEL = "cancel" + + +class ExecutionStatus(str, enum.Enum): + CREATED = "created" + APPROVED = "approved" + REJECTED = "rejected" + SUBMITTED = "submitted" + PARTIALLY_FILLED = "partially_filled" + FILLED = "filled" + CANCELLED = "cancelled" + FAILED = "failed" + TRIGGERED = "triggered" + + +class RiskVerdict(str, enum.Enum): + APPROVE = "approve" + THROTTLE = "throttle" + REJECT = "reject" + + +@dataclass(slots=True) +class Instrument: + symbol: str + asset_type: AssetType = AssetType.STOCK + exchange: str = "SMART" + currency: str = "USD" + primary_exchange: Optional[str] = None + expiry: Optional[str] = None + multiplier: Optional[str] = None + + +@dataclass(slots=True) +class DecisionContext: + source: IntentSource + strategy_id: str + strategy_version: str + model_family: str = "rules" + signal_strength: float = 0.0 + confidence: float = 0.0 + rationale: str = "" + tags: List[str] = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass(slots=True) +class RiskEnvelope: + max_notional: float + max_position_qty: float + max_daily_loss: float + max_symbol_exposure: float + max_order_risk: float + min_confidence: float = 0.0 + spread_limit_bps: Optional[float] = None + slippage_limit_bps: Optional[float] = None + volatility_cap: Optional[float] = None + allow_short: bool = False + allow_fractional: bool = True + allow_after_hours: bool = False + kill_switch_enabled: bool = True + + +@dataclass(slots=True) +class OrderIntent: + instrument: Instrument + action: DecisionAction + side: OrderSide + quantity: float + order_type: OrderType = OrderType.MARKET + tif: TimeInForce = TimeInForce.DAY + limit_price: Optional[float] = None + stop_price: Optional[float] = None + take_profit_price: Optional[float] = None + trailing_percent: Optional[float] = None + trailing_amount: Optional[float] = None + decision: Optional[DecisionContext] = None + risk: Optional[RiskEnvelope] = None + parent_client_order_id: Optional[str] = None + client_order_id: str = field(default_factory=lambda: f"alice-{uuid.uuid4().hex[:16]}") + created_at_utc: str = field(default_factory=lambda: dt.datetime.now(dt.timezone.utc).isoformat()) + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass(slots=True) +class RiskCheckResult: + verdict: RiskVerdict + approved_quantity: float + reasons: List[str] = field(default_factory=list) + metrics: Dict[str, Any] = field(default_factory=dict) + + +@dataclass(slots=True) +class ExecutionReport: + execution_id: str + client_order_id: str + broker_order_id: Optional[str] + status: ExecutionStatus + symbol: str + side: str + quantity: float + filled_quantity: float = 0.0 + avg_fill_price: Optional[float] = None + message: str = "" + raw: Dict[str, Any] = field(default_factory=dict) + created_at_utc: str = field(default_factory=lambda: dt.datetime.now(dt.timezone.utc).isoformat()) + updated_at_utc: str = field(default_factory=lambda: dt.datetime.now(dt.timezone.utc).isoformat()) + + +@dataclass(slots=True) +class PositionSnapshot: + symbol: str + quantity: float + avg_cost: float + market_price: Optional[float] = None + market_value: Optional[float] = None + unrealized_pnl: Optional[float] = None + realized_pnl: Optional[float] = None + account: Optional[str] = None + + +@dataclass(slots=True) +class AccountSnapshot: + account_id: str + net_liquidation: float = 0.0 + available_funds: float = 0.0 + buying_power: float = 0.0 + excess_liquidity: float = 0.0 + realized_pnl: float = 0.0 + unrealized_pnl: float = 0.0 + timestamp_utc: str = field(default_factory=lambda: dt.datetime.now(dt.timezone.utc).isoformat()) + + +@dataclass(slots=True) +class OpenOrder: + client_order_id: str + symbol: str + side: str + quantity: float + order_type: str + status: str + broker_order_id: Optional[str] = None + limit_price: Optional[float] = None + stop_price: Optional[float] = None + take_profit_price: Optional[float] = None + parent_client_order_id: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass(slots=True) +class BracketState: + entry_client_order_id: str + symbol: str + side: OrderSide + quantity: float + stop_price: Optional[float] = None + take_profit_price: Optional[float] = None + active: bool = True + + +@dataclass(slots=True) +class BracketSubmissionResult: + parent: ExecutionReport + take_profit: Optional[ExecutionReport] = None + stop_loss: Optional[ExecutionReport] = None + + +# ============================================================================= +# PERSISTENCE LAYER +# ============================================================================= + +class ExecutionJournal: + def __init__(self, db_path: Union[str, Path] = "alice_execution_journal.db") -> None: + self.db_path = str(db_path) + self._lock = threading.Lock() + self._init_db() + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + return conn + + def _init_db(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS execution_reports ( + execution_id TEXT PRIMARY KEY, + client_order_id TEXT NOT NULL, + broker_order_id TEXT, + status TEXT NOT NULL, + symbol TEXT NOT NULL, + side TEXT NOT NULL, + quantity REAL NOT NULL, + filled_quantity REAL NOT NULL, + avg_fill_price REAL, + message TEXT, + raw_json TEXT, + created_at_utc TEXT NOT NULL, + updated_at_utc TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS order_intents ( + client_order_id TEXT PRIMARY KEY, + payload_json TEXT NOT NULL, + created_at_utc TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS risk_checks ( + client_order_id TEXT NOT NULL, + verdict TEXT NOT NULL, + approved_quantity REAL NOT NULL, + reasons_json TEXT NOT NULL, + metrics_json TEXT NOT NULL, + created_at_utc TEXT NOT NULL + ) + """ + ) + conn.commit() + + def write_intent(self, intent: OrderIntent) -> None: + with self._lock, self._connect() as conn: + conn.execute( + """ + INSERT OR REPLACE INTO order_intents (client_order_id, payload_json, created_at_utc) + VALUES (?, ?, ?) + """, + (intent.client_order_id, json.dumps(to_jsonable(intent), default=str), intent.created_at_utc), + ) + conn.commit() + + def write_risk_check(self, client_order_id: str, result: RiskCheckResult) -> None: + with self._lock, self._connect() as conn: + conn.execute( + """ + INSERT INTO risk_checks ( + client_order_id, verdict, approved_quantity, reasons_json, metrics_json, created_at_utc + ) VALUES (?, ?, ?, ?, ?, ?) + """, + ( + client_order_id, + result.verdict.value, + result.approved_quantity, + json.dumps(result.reasons), + json.dumps(result.metrics), + utc_now(), + ), + ) + conn.commit() + + def write_report(self, report: ExecutionReport) -> None: + with self._lock, self._connect() as conn: + conn.execute( + """ + INSERT OR REPLACE INTO execution_reports ( + execution_id, client_order_id, broker_order_id, status, symbol, side, + quantity, filled_quantity, avg_fill_price, message, raw_json, + created_at_utc, updated_at_utc + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + report.execution_id, + report.client_order_id, + report.broker_order_id, + report.status.value, + report.symbol, + report.side, + report.quantity, + report.filled_quantity, + report.avg_fill_price, + report.message, + json.dumps(report.raw, default=str), + report.created_at_utc, + report.updated_at_utc, + ), + ) + conn.commit() + + +# ============================================================================= +# HELPERS +# ============================================================================= + +def to_jsonable(obj: Any) -> Any: + if dataclasses.is_dataclass(obj): + return {k: to_jsonable(v) for k, v in asdict(obj).items()} + if isinstance(obj, enum.Enum): + return obj.value + if isinstance(obj, dict): + return {str(k): to_jsonable(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple, set)): + return [to_jsonable(x) for x in obj] + return obj + + +def utc_now() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat() + + +def normalize_status(status_text: str) -> ExecutionStatus: + normalized = (status_text or "").lower() + if "fill" in normalized: + return ExecutionStatus.FILLED + if "partial" in normalized: + return ExecutionStatus.PARTIALLY_FILLED + if "cancel" in normalized: + return ExecutionStatus.CANCELLED + if "trigger" in normalized: + return ExecutionStatus.TRIGGERED + if "reject" in normalized or "inactive" in normalized or "error" in normalized: + return ExecutionStatus.FAILED + return ExecutionStatus.SUBMITTED + + +def validate_intent(intent: OrderIntent) -> None: + if intent.quantity < 0: + raise ValueError("OrderIntent.quantity must be non-negative.") + if intent.action in {DecisionAction.HOLD, DecisionAction.CANCEL} and intent.quantity != 0: + raise ValueError("HOLD and CANCEL intents must have quantity 0.") + if intent.order_type == OrderType.LIMIT and intent.limit_price is None: + raise ValueError("LIMIT order requires limit_price.") + if intent.order_type == OrderType.STOP and intent.stop_price is None: + raise ValueError("STOP order requires stop_price.") + if intent.order_type == OrderType.STOP_LIMIT and (intent.stop_price is None or intent.limit_price is None): + raise ValueError("STOP_LIMIT order requires stop_price and limit_price.") + if intent.trailing_percent is not None and intent.trailing_percent <= 0: + raise ValueError("trailing_percent must be positive.") + if intent.trailing_amount is not None and intent.trailing_amount <= 0: + raise ValueError("trailing_amount must be positive.") + + +# ============================================================================= +# EVENT BUS +# ============================================================================= + +class EventHook: + def __init__(self) -> None: + self._listeners: List[Callable[[Dict[str, Any]], None]] = [] + + def subscribe(self, callback: Callable[[Dict[str, Any]], None]) -> None: + self._listeners.append(callback) + + def unsubscribe(self, callback: Callable[[Dict[str, Any]], None]) -> None: + self._listeners = [fn for fn in self._listeners if fn is not callback] + + def emit(self, event: Dict[str, Any]) -> None: + for listener in list(self._listeners): + try: + listener(event) + except Exception as exc: + logger.exception("Event listener failed: %s", exc) + + +# ============================================================================= +# RISK ENGINE +# ============================================================================= + +class RiskEngine: + """ + Evaluates an OrderIntent against a RiskEnvelope and account state. + + Checks performed + ---------------- + 1. Kill-switch gate + 2. Min confidence threshold + 3. Short-selling permission + 4. Available cash vs notional cost + 5. Notional cap per order + 6. Symbol exposure cap + 7. Max position quantity + 8. Order-risk cap (stop-distance × qty) + 9. Daily loss limit + """ + + def __init__(self, journal: Optional[ExecutionJournal] = None) -> None: + self._journal = journal + + def check( + self, + intent: OrderIntent, + account: AccountSnapshot, + current_positions: Optional[Dict[str, PositionSnapshot]] = None, + daily_realized_loss: float = 0.0, + market_price: Optional[float] = None, + ) -> RiskCheckResult: + risk = intent.risk + reasons: List[str] = [] + metrics: Dict[str, Any] = {} + + if risk is None: + return RiskCheckResult( + verdict=RiskVerdict.APPROVE, + approved_quantity=intent.quantity, + reasons=["No risk envelope provided; auto-approved."], + metrics=metrics, + ) + + qty = intent.quantity + approved_qty = qty + + # 1. Kill-switch + if risk.kill_switch_enabled and os.environ.get("ALICE_KILL_SWITCH", "0") == "1": + reasons.append("Kill switch is active.") + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + # 2. Confidence gate + confidence = intent.decision.confidence if intent.decision else 0.0 + metrics["confidence"] = confidence + if confidence < risk.min_confidence: + reasons.append( + f"Confidence {confidence:.3f} below minimum {risk.min_confidence:.3f}." + ) + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + # 3. Short-selling permission + if intent.side == OrderSide.SELL and intent.action in { + DecisionAction.ENTER_SHORT + } and not risk.allow_short: + reasons.append("Short selling is not permitted by this risk envelope.") + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + # 4 & 5. Notional checks + ref_price = ( + market_price + or intent.limit_price + or intent.stop_price + or 0.0 + ) + notional = ref_price * qty + metrics["ref_price"] = ref_price + metrics["notional"] = notional + metrics["available_funds"] = account.available_funds + + if notional > risk.max_notional: + # Throttle quantity to fit within notional cap + if ref_price > 0: + capped_qty = risk.max_notional / ref_price + if not risk.allow_fractional: + capped_qty = float(int(capped_qty)) + approved_qty = min(approved_qty, capped_qty) + reasons.append( + f"Notional {notional:.2f} exceeds cap {risk.max_notional:.2f}; " + f"quantity throttled to {approved_qty}." + ) + else: + reasons.append("Cannot compute notional without a reference price; order rejected.") + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + # 4b. Cash availability + order_cost = ref_price * approved_qty + if intent.side == OrderSide.BUY and order_cost > account.available_funds: + if ref_price > 0 and account.available_funds > 0: + cash_qty = account.available_funds / ref_price + if not risk.allow_fractional: + cash_qty = float(int(cash_qty)) + approved_qty = min(approved_qty, cash_qty) + reasons.append( + f"Insufficient cash ({account.available_funds:.2f}); " + f"quantity throttled to {approved_qty}." + ) + else: + reasons.append("Insufficient cash and cannot throttle; order rejected.") + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + # 6. Symbol exposure cap + current_pos = (current_positions or {}).get(intent.instrument.symbol) + existing_qty = abs(current_pos.quantity) if current_pos else 0.0 + metrics["existing_position_qty"] = existing_qty + if existing_qty + approved_qty > risk.max_symbol_exposure: + allowed = max(0.0, risk.max_symbol_exposure - existing_qty) + if not risk.allow_fractional: + allowed = float(int(allowed)) + approved_qty = min(approved_qty, allowed) + reasons.append( + f"Symbol exposure cap {risk.max_symbol_exposure} reached; " + f"quantity throttled to {approved_qty}." + ) + + # 7. Max position quantity + if approved_qty > risk.max_position_qty: + approved_qty = risk.max_position_qty + if not risk.allow_fractional: + approved_qty = float(int(approved_qty)) + reasons.append( + f"Max position qty {risk.max_position_qty} applied." + ) + + # 8. Stop-based order risk + if intent.stop_price is not None and ref_price > 0: + stop_distance = abs(ref_price - intent.stop_price) + order_risk = stop_distance * approved_qty + metrics["stop_distance"] = stop_distance + metrics["order_risk"] = order_risk + if order_risk > risk.max_order_risk: + if stop_distance > 0: + risk_qty = risk.max_order_risk / stop_distance + if not risk.allow_fractional: + risk_qty = float(int(risk_qty)) + approved_qty = min(approved_qty, risk_qty) + reasons.append( + f"Order risk {order_risk:.2f} exceeds cap {risk.max_order_risk:.2f}; " + f"quantity throttled to {approved_qty}." + ) + + # 9. Daily loss limit + metrics["daily_realized_loss"] = daily_realized_loss + if abs(daily_realized_loss) >= risk.max_daily_loss: + reasons.append( + f"Daily loss limit {risk.max_daily_loss:.2f} reached " + f"(current: {daily_realized_loss:.2f}); order rejected." + ) + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons, + metrics=metrics, + ) + + if approved_qty <= 0: + return RiskCheckResult( + verdict=RiskVerdict.REJECT, + approved_quantity=0.0, + reasons=reasons or ["Approved quantity reduced to zero."], + metrics=metrics, + ) + + verdict = RiskVerdict.THROTTLE if approved_qty < qty else RiskVerdict.APPROVE + return RiskCheckResult( + verdict=verdict, + approved_quantity=approved_qty, + reasons=reasons, + metrics=metrics, + ) + + +# ============================================================================= +# ABSTRACT BROKER ADAPTER +# ============================================================================= + +class BrokerAdapter(abc.ABC): + """ + Broker-agnostic interface that all broker implementations must satisfy. + """ + + on_fill: EventHook + on_reject: EventHook + on_cancel: EventHook + on_status_change: EventHook + + @abc.abstractmethod + def submit_order(self, intent: OrderIntent) -> ExecutionReport: + """Submit a single order intent and return the initial execution report.""" + + @abc.abstractmethod + def submit_bracket(self, intent: OrderIntent) -> BracketSubmissionResult: + """ + Submit an entry order together with protective stop-loss and + take-profit child orders. + """ + + @abc.abstractmethod + def cancel_order(self, client_order_id: str) -> ExecutionReport: + """Request cancellation of an open order by client_order_id.""" + + @abc.abstractmethod + def get_open_orders(self) -> List[OpenOrder]: + """Return all open orders tracked by this adapter.""" + + @abc.abstractmethod + def get_positions(self) -> List[PositionSnapshot]: + """Return current position snapshots.""" + + @abc.abstractmethod + def get_account(self) -> AccountSnapshot: + """Return a current account snapshot.""" + + @abc.abstractmethod + def health_check(self) -> Dict[str, Any]: + """Return a dict describing broker connectivity / health.""" + + @abc.abstractmethod + def connect(self) -> None: + """Establish connection to broker.""" + + @abc.abstractmethod + def disconnect(self) -> None: + """Tear down connection to broker.""" + + +# ============================================================================= +# PAPER BROKER +# ============================================================================= + +class _PaperPosition: + """Internal mutable position tracker for the paper broker.""" + + def __init__(self, symbol: str, quantity: float, avg_cost: float) -> None: + self.symbol = symbol + self.quantity = quantity + self.avg_cost = avg_cost + self.realized_pnl: float = 0.0 + + def apply_fill(self, side: OrderSide, qty: float, fill_price: float) -> float: + """Update position after a fill. Returns realized PnL from this fill.""" + realized = 0.0 + if side == OrderSide.BUY: + if self.quantity < 0: + # Covering a short + closing = min(qty, abs(self.quantity)) + realized = closing * (self.avg_cost - fill_price) + self.quantity += closing + qty -= closing + if qty > 0: + # Adding to long + total_cost = self.avg_cost * max(self.quantity, 0) + fill_price * qty + self.quantity += qty + self.avg_cost = total_cost / self.quantity if self.quantity != 0 else fill_price + else: # SELL + if self.quantity > 0: + # Closing a long + closing = min(qty, self.quantity) + realized = closing * (fill_price - self.avg_cost) + self.quantity -= closing + qty -= closing + if qty > 0: + # Opening or adding to short + if self.quantity == 0: + self.avg_cost = fill_price + else: + total_cost = self.avg_cost * abs(self.quantity) + fill_price * qty + self.quantity -= qty + self.avg_cost = total_cost / abs(self.quantity) if self.quantity != 0 else fill_price + qty = 0 + if qty > 0: + self.quantity -= qty + self.avg_cost = fill_price + self.realized_pnl += realized + return realized + + def unrealized_pnl(self, market_price: float) -> float: + if self.quantity == 0: + return 0.0 + return self.quantity * (market_price - self.avg_cost) + + +class PaperBroker(BrokerAdapter): + """ + In-memory paper trading broker. + + Features + -------- + - Instant market-order fills at the provided (or simulated) price + - Limit/stop order simulation via a background price-update loop + - Bracket order protective logic (stop-loss + take-profit) + - Realized and unrealized PnL tracking per position + - Open-order registry with cancel support + - Event emission on fill, reject, cancel, status change + """ + + DEFAULT_INITIAL_CASH = 100_000.0 + _PRICE_LOOP_INTERVAL = 2.0 # seconds + + def __init__( + self, + account_id: str = "PAPER-0001", + initial_cash: float = DEFAULT_INITIAL_CASH, + journal: Optional[ExecutionJournal] = None, + risk_engine: Optional[RiskEngine] = None, + ) -> None: + self._account_id = account_id + self._cash = initial_cash + self._initial_cash = initial_cash + self._journal = journal + self._risk_engine = risk_engine or RiskEngine(journal) + self._lock = threading.Lock() + + self._positions: Dict[str, _PaperPosition] = {} + self._open_orders: Dict[str, OpenOrder] = {} + self._brackets: Dict[str, BracketState] = {} + self._market_prices: Dict[str, float] = {} + self._daily_realized_loss: float = 0.0 + self._total_realized_pnl: float = 0.0 + + self.on_fill = EventHook() + self.on_reject = EventHook() + self.on_cancel = EventHook() + self.on_status_change = EventHook() + + self._price_loop_thread: Optional[threading.Thread] = None + self._price_loop_running = False + + # ------------------------------------------------------------------ + # Connection lifecycle + # ------------------------------------------------------------------ + + def connect(self) -> None: + self._price_loop_running = True + self._price_loop_thread = threading.Thread( + target=self._price_update_loop, daemon=True, name="paper-price-loop" + ) + self._price_loop_thread.start() + logger.info("PaperBroker connected. Account: %s Cash: %.2f", self._account_id, self._cash) + + def disconnect(self) -> None: + self._price_loop_running = False + if self._price_loop_thread: + self._price_loop_thread.join(timeout=5.0) + logger.info("PaperBroker disconnected.") + + # ------------------------------------------------------------------ + # Price management + # ------------------------------------------------------------------ + + def update_price(self, symbol: str, price: float) -> None: + """Feed a market price for a symbol; triggers pending order evaluation.""" + with self._lock: + self._market_prices[symbol] = price + + def _price_update_loop(self) -> None: + while self._price_loop_running: + try: + self._evaluate_pending_orders() + except Exception as exc: + logger.exception("Price loop error: %s", exc) + time.sleep(self._PRICE_LOOP_INTERVAL) + + def _evaluate_pending_orders(self) -> None: + with self._lock: + pending = [ + o for o in list(self._open_orders.values()) + if o.status in ("submitted", "partially_filled") + ] + for order in pending: + price = self._market_prices.get(order.symbol) + if price is None: + continue + self._try_fill_pending(order, price) + + def _try_fill_pending(self, order: OpenOrder, price: float) -> None: + triggered = False + if order.order_type == OrderType.LIMIT.value: + if order.side == OrderSide.BUY.value and price <= (order.limit_price or float("inf")): + triggered = True + elif order.side == OrderSide.SELL.value and price >= (order.limit_price or 0): + triggered = True + elif order.order_type == OrderType.STOP.value: + if order.side == OrderSide.BUY.value and price >= (order.stop_price or 0): + triggered = True + elif order.side == OrderSide.SELL.value and price <= (order.stop_price or float("inf")): + triggered = True + elif order.order_type == OrderType.STOP_LIMIT.value: + if order.side == OrderSide.BUY.value and price >= (order.stop_price or 0): + if price <= (order.limit_price or float("inf")): + triggered = True + elif order.side == OrderSide.SELL.value and price <= (order.stop_price or float("inf")): + if price >= (order.limit_price or 0): + triggered = True + + if triggered: + fill_price = price + self._execute_fill(order.client_order_id, fill_price) + + # ------------------------------------------------------------------ + # Order submission + # ------------------------------------------------------------------ + + def submit_order(self, intent: OrderIntent) -> ExecutionReport: + try: + validate_intent(intent) + except ValueError as exc: + report = self._make_report(intent, ExecutionStatus.REJECTED, message=str(exc)) + self._emit_reject(report, str(exc)) + return report + + account = self.get_account() + risk_result = self._risk_engine.check( + intent, + account, + current_positions={s: self._pos_snapshot(p) for s, p in self._positions.items()}, + daily_realized_loss=self._daily_realized_loss, + market_price=self._market_prices.get(intent.instrument.symbol), + ) + + if self._journal: + self._journal.write_intent(intent) + self._journal.write_risk_check(intent.client_order_id, risk_result) + + if risk_result.verdict == RiskVerdict.REJECT: + report = self._make_report( + intent, + ExecutionStatus.REJECTED, + message="; ".join(risk_result.reasons), + ) + self._emit_reject(report, report.message) + if self._journal: + self._journal.write_report(report) + return report + + approved_qty = risk_result.approved_quantity + + with self._lock: + open_order = OpenOrder( + client_order_id=intent.client_order_id, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=approved_qty, + order_type=intent.order_type.value, + status=ExecutionStatus.SUBMITTED.value, + limit_price=intent.limit_price, + stop_price=intent.stop_price, + take_profit_price=intent.take_profit_price, + parent_client_order_id=intent.parent_client_order_id, + metadata={"risk_reasons": risk_result.reasons}, + ) + self._open_orders[intent.client_order_id] = open_order + + report = self._make_report(intent, ExecutionStatus.SUBMITTED, quantity=approved_qty) + self.on_status_change.emit({"event": "submitted", "report": to_jsonable(report)}) + + if self._journal: + self._journal.write_report(report) + + # Immediate fill for market orders + if intent.order_type == OrderType.MARKET: + fill_price = self._market_prices.get(intent.instrument.symbol, 0.0) + if fill_price <= 0: + fill_price = intent.limit_price or intent.stop_price or 1.0 + fill_report = self._execute_fill(intent.client_order_id, fill_price) + if fill_report is not None: + return fill_report + + return report + + def submit_bracket(self, intent: OrderIntent) -> BracketSubmissionResult: + """ + Submit entry order. If stop_price or take_profit_price are set on the + intent, register protective child orders that will be submitted once the + parent fill is confirmed. + """ + parent_report = self.submit_order(intent) + + if parent_report.status in {ExecutionStatus.REJECTED, ExecutionStatus.FAILED}: + return BracketSubmissionResult(parent=parent_report) + + with self._lock: + bracket = BracketState( + entry_client_order_id=intent.client_order_id, + symbol=intent.instrument.symbol, + side=intent.side, + quantity=intent.quantity, + stop_price=intent.stop_price, + take_profit_price=intent.take_profit_price, + ) + self._brackets[intent.client_order_id] = bracket + + return BracketSubmissionResult(parent=parent_report) + + # ------------------------------------------------------------------ + # Fill execution + # ------------------------------------------------------------------ + + def _execute_fill(self, client_order_id: str, fill_price: float) -> Optional[ExecutionReport]: + with self._lock: + order = self._open_orders.get(client_order_id) + if order is None or order.status not in ("submitted", "partially_filled"): + return None + + side = OrderSide(order.side) + qty = order.quantity + + # Update cash + if side == OrderSide.BUY: + self._cash -= fill_price * qty + else: + self._cash += fill_price * qty + + # Update position + pos = self._positions.setdefault(order.symbol, _PaperPosition(order.symbol, 0.0, 0.0)) + realized = pos.apply_fill(side, qty, fill_price) + self._total_realized_pnl += realized + if realized < 0: + self._daily_realized_loss += abs(realized) + + # Remove zero positions + if pos.quantity == 0.0: + del self._positions[order.symbol] + + order.status = ExecutionStatus.FILLED.value + + execution_id = f"fill-{uuid.uuid4().hex[:12]}" + report = ExecutionReport( + execution_id=execution_id, + client_order_id=client_order_id, + broker_order_id=None, + status=ExecutionStatus.FILLED, + symbol=order.symbol, + side=order.side, + quantity=qty, + filled_quantity=qty, + avg_fill_price=fill_price, + message="Paper fill", + updated_at_utc=utc_now(), + ) + + if self._journal: + self._journal.write_report(report) + + self.on_fill.emit( + { + "event": "fill", + "client_order_id": client_order_id, + "symbol": order.symbol, + "side": order.side, + "qty": qty, + "fill_price": fill_price, + "realized_pnl": realized if order.side == OrderSide.SELL.value else 0.0, + "report": to_jsonable(report), + } + ) + + # Activate bracket protection + self._check_bracket_activation(client_order_id, fill_price) + + return report + + def _check_bracket_activation(self, entry_client_order_id: str, entry_fill_price: float) -> None: + bracket = self._brackets.get(entry_client_order_id) + if bracket is None or not bracket.active: + return + + bracket.active = False + protective_side = OrderSide.SELL if bracket.side == OrderSide.BUY else OrderSide.BUY + action = DecisionAction.EXIT_LONG if bracket.side == OrderSide.BUY else DecisionAction.EXIT_SHORT + + if bracket.stop_price is not None: + stop_intent = OrderIntent( + instrument=Instrument(symbol=bracket.symbol), + action=action, + side=protective_side, + quantity=bracket.quantity, + order_type=OrderType.STOP, + stop_price=bracket.stop_price, + parent_client_order_id=entry_client_order_id, + ) + self.submit_order(stop_intent) + + if bracket.take_profit_price is not None: + tp_intent = OrderIntent( + instrument=Instrument(symbol=bracket.symbol), + action=action, + side=protective_side, + quantity=bracket.quantity, + order_type=OrderType.LIMIT, + limit_price=bracket.take_profit_price, + parent_client_order_id=entry_client_order_id, + ) + self.submit_order(tp_intent) + + # ------------------------------------------------------------------ + # Cancel + # ------------------------------------------------------------------ + + def cancel_order(self, client_order_id: str) -> ExecutionReport: + with self._lock: + order = self._open_orders.get(client_order_id) + if order is None: + return ExecutionReport( + execution_id=f"cancel-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=None, + status=ExecutionStatus.FAILED, + symbol="UNKNOWN", + side="", + quantity=0.0, + message="Order not found.", + ) + if order.status in (ExecutionStatus.FILLED.value, ExecutionStatus.CANCELLED.value): + return ExecutionReport( + execution_id=f"cancel-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=None, + status=ExecutionStatus.FAILED, + symbol=order.symbol, + side=order.side, + quantity=order.quantity, + message=f"Cannot cancel order with status: {order.status}.", + ) + order.status = ExecutionStatus.CANCELLED.value + + report = ExecutionReport( + execution_id=f"cancel-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=None, + status=ExecutionStatus.CANCELLED, + symbol=order.symbol, + side=order.side, + quantity=order.quantity, + message="Cancelled by request.", + updated_at_utc=utc_now(), + ) + + if self._journal: + self._journal.write_report(report) + + self.on_cancel.emit({"event": "cancel", "client_order_id": client_order_id, "report": to_jsonable(report)}) + return report + + # ------------------------------------------------------------------ + # Query + # ------------------------------------------------------------------ + + def get_open_orders(self) -> List[OpenOrder]: + with self._lock: + return [ + o for o in self._open_orders.values() + if o.status in (ExecutionStatus.SUBMITTED.value, ExecutionStatus.PARTIALLY_FILLED.value) + ] + + def get_positions(self) -> List[PositionSnapshot]: + with self._lock: + return [self._pos_snapshot(p) for p in self._positions.values()] + + def get_account(self) -> AccountSnapshot: + with self._lock: + total_market_value = 0.0 + total_unrealized = 0.0 + for symbol, pos in self._positions.items(): + mp = self._market_prices.get(symbol, pos.avg_cost) + mv = pos.quantity * mp + total_market_value += mv + total_unrealized += pos.unrealized_pnl(mp) + + net_liq = self._cash + total_market_value + return AccountSnapshot( + account_id=self._account_id, + net_liquidation=net_liq, + available_funds=self._cash, + buying_power=self._cash, + excess_liquidity=self._cash, + realized_pnl=self._total_realized_pnl, + unrealized_pnl=total_unrealized, + ) + + def health_check(self) -> Dict[str, Any]: + return { + "broker": "PaperBroker", + "connected": self._price_loop_running, + "account_id": self._account_id, + "cash": self._cash, + "open_orders": len(self.get_open_orders()), + "positions": len(self._positions), + "timestamp_utc": utc_now(), + } + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _make_report( + self, + intent: OrderIntent, + status: ExecutionStatus, + message: str = "", + quantity: Optional[float] = None, + ) -> ExecutionReport: + return ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=intent.client_order_id, + broker_order_id=None, + status=status, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=quantity if quantity is not None else intent.quantity, + message=message, + ) + + def _pos_snapshot(self, pos: _PaperPosition) -> PositionSnapshot: + mp = self._market_prices.get(pos.symbol, pos.avg_cost) + return PositionSnapshot( + symbol=pos.symbol, + quantity=pos.quantity, + avg_cost=pos.avg_cost, + market_price=mp, + market_value=pos.quantity * mp, + unrealized_pnl=pos.unrealized_pnl(mp), + realized_pnl=pos.realized_pnl, + account=self._account_id, + ) + + def _emit_reject(self, report: ExecutionReport, reason: str) -> None: + self.on_reject.emit({"event": "reject", "reason": reason, "report": to_jsonable(report)}) + + +# ============================================================================= +# IBKR BROKER ADAPTER +# ============================================================================= + +class IBKRBroker(BrokerAdapter): + """ + Interactive Brokers adapter backed by ib_insync. + + Usage + ----- + 1. Ensure TWS or IB Gateway is running with API access enabled. + 2. Instantiate with environment=Environment.PAPER for paper trading. + 3. Call connect() before submitting any orders. + 4. Always call disconnect() on shutdown. + + This is a scaffold. Real-money usage requires thorough testing in + IBKR paper mode first. See ib_insync docs for advanced configuration. + """ + + def __init__( + self, + host: str = "127.0.0.1", + port: int = 7497, + client_id: int = 1, + environment: Environment = Environment.PAPER, + journal: Optional[ExecutionJournal] = None, + risk_engine: Optional[RiskEngine] = None, + readonly: bool = False, + timeout: float = 30.0, + ) -> None: + if not IB_INSYNC_AVAILABLE: + raise RuntimeError( + "ib_insync is not installed. Install it with: pip install ib_insync" + ) + + self._host = host + self._port = port + self._client_id = client_id + self._environment = environment + self._journal = journal + self._risk_engine = risk_engine or RiskEngine(journal) + self._readonly = readonly + self._timeout = timeout + + self._ib: Any = IB() + self._lock = threading.Lock() + self._open_orders: Dict[str, OpenOrder] = {} + self._client_to_broker: Dict[str, int] = {} + + self.on_fill = EventHook() + self.on_reject = EventHook() + self.on_cancel = EventHook() + self.on_status_change = EventHook() + + # ------------------------------------------------------------------ + # Connection lifecycle + # ------------------------------------------------------------------ + + def connect(self) -> None: + self._ib.connect( + host=self._host, + port=self._port, + clientId=self._client_id, + readonly=self._readonly, + timeout=self._timeout, + ) + self._ib.orderStatusEvent += self._on_order_status + self._ib.errorEvent += self._on_error + logger.info( + "IBKRBroker connected. host=%s port=%d clientId=%d env=%s", + self._host, self._port, self._client_id, self._environment.value, + ) + + def disconnect(self) -> None: + try: + self._ib.disconnect() + except Exception as exc: + logger.warning("IBKRBroker disconnect warning: %s", exc) + logger.info("IBKRBroker disconnected.") + + # ------------------------------------------------------------------ + # Order submission + # ------------------------------------------------------------------ + + def submit_order(self, intent: OrderIntent) -> ExecutionReport: + try: + validate_intent(intent) + except ValueError as exc: + report = self._rejected_report(intent, str(exc)) + self._emit_reject(report, str(exc)) + return report + + account = self.get_account() + risk_result = self._risk_engine.check(intent, account) + + if self._journal: + self._journal.write_intent(intent) + self._journal.write_risk_check(intent.client_order_id, risk_result) + + if risk_result.verdict == RiskVerdict.REJECT: + msg = "; ".join(risk_result.reasons) + report = self._rejected_report(intent, msg) + self._emit_reject(report, msg) + if self._journal: + self._journal.write_report(report) + return report + + contract = self._build_contract(intent.instrument) + ib_order = self._build_ib_order(intent, risk_result.approved_quantity) + + trade = self._ib.placeOrder(contract, ib_order) + broker_order_id = str(trade.order.orderId) + + with self._lock: + self._client_to_broker[intent.client_order_id] = trade.order.orderId + self._open_orders[intent.client_order_id] = OpenOrder( + client_order_id=intent.client_order_id, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=risk_result.approved_quantity, + order_type=intent.order_type.value, + status=ExecutionStatus.SUBMITTED.value, + broker_order_id=broker_order_id, + limit_price=intent.limit_price, + stop_price=intent.stop_price, + take_profit_price=intent.take_profit_price, + ) + + report = ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=intent.client_order_id, + broker_order_id=broker_order_id, + status=ExecutionStatus.SUBMITTED, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=risk_result.approved_quantity, + message="Order submitted to IBKR.", + ) + + if self._journal: + self._journal.write_report(report) + + self.on_status_change.emit({"event": "submitted", "report": to_jsonable(report)}) + return report + + def submit_bracket(self, intent: OrderIntent) -> BracketSubmissionResult: + """ + Submit a bracket order (entry + protective stop + take-profit) + using IBKR's native parent/child order mechanism. + """ + try: + validate_intent(intent) + except ValueError as exc: + report = self._rejected_report(intent, str(exc)) + self._emit_reject(report, str(exc)) + return BracketSubmissionResult(parent=report) + + account = self.get_account() + risk_result = self._risk_engine.check(intent, account) + + if self._journal: + self._journal.write_intent(intent) + self._journal.write_risk_check(intent.client_order_id, risk_result) + + if risk_result.verdict == RiskVerdict.REJECT: + msg = "; ".join(risk_result.reasons) + report = self._rejected_report(intent, msg) + self._emit_reject(report, msg) + if self._journal: + self._journal.write_report(report) + return BracketSubmissionResult(parent=report) + + approved_qty = risk_result.approved_quantity + contract = self._build_contract(intent.instrument) + protective_side = "SELL" if intent.side == OrderSide.BUY else "BUY" + + parent_order = self._build_ib_order(intent, approved_qty) + parent_order.transmit = False + + tp_report: Optional[ExecutionReport] = None + sl_report: Optional[ExecutionReport] = None + + take_profit_order = None + stop_loss_order = None + + if intent.take_profit_price is not None: + take_profit_order = LimitOrder( + action=protective_side, + totalQuantity=approved_qty, + lmtPrice=intent.take_profit_price, + tif=intent.tif.value, + ) + take_profit_order.parentId = parent_order.orderId + take_profit_order.transmit = False + + if intent.stop_price is not None: + stop_loss_order = StopOrder( + action=protective_side, + totalQuantity=approved_qty, + stopPrice=intent.stop_price, + tif=intent.tif.value, + ) + stop_loss_order.parentId = parent_order.orderId + stop_loss_order.transmit = True # transmits the whole bracket + elif take_profit_order is not None: + take_profit_order.transmit = True # transmit if no stop + + parent_trade = self._ib.placeOrder(contract, parent_order) + parent_broker_id = str(parent_trade.order.orderId) + + with self._lock: + self._client_to_broker[intent.client_order_id] = parent_trade.order.orderId + self._open_orders[intent.client_order_id] = OpenOrder( + client_order_id=intent.client_order_id, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=approved_qty, + order_type=intent.order_type.value, + status=ExecutionStatus.SUBMITTED.value, + broker_order_id=parent_broker_id, + limit_price=intent.limit_price, + stop_price=intent.stop_price, + take_profit_price=intent.take_profit_price, + ) + + parent_report = ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=intent.client_order_id, + broker_order_id=parent_broker_id, + status=ExecutionStatus.SUBMITTED, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=approved_qty, + message="Bracket parent submitted to IBKR.", + ) + + if take_profit_order is not None: + tp_trade = self._ib.placeOrder(contract, take_profit_order) + tp_coid = f"alice-tp-{uuid.uuid4().hex[:12]}" + tp_report = ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=tp_coid, + broker_order_id=str(tp_trade.order.orderId), + status=ExecutionStatus.SUBMITTED, + symbol=intent.instrument.symbol, + side=protective_side, + quantity=approved_qty, + message="Bracket take-profit submitted.", + ) + + if stop_loss_order is not None: + sl_trade = self._ib.placeOrder(contract, stop_loss_order) + sl_coid = f"alice-sl-{uuid.uuid4().hex[:12]}" + sl_report = ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=sl_coid, + broker_order_id=str(sl_trade.order.orderId), + status=ExecutionStatus.SUBMITTED, + symbol=intent.instrument.symbol, + side=protective_side, + quantity=approved_qty, + message="Bracket stop-loss submitted.", + ) + + if self._journal: + self._journal.write_report(parent_report) + if tp_report: + self._journal.write_report(tp_report) + if sl_report: + self._journal.write_report(sl_report) + + return BracketSubmissionResult( + parent=parent_report, + take_profit=tp_report, + stop_loss=sl_report, + ) + + # ------------------------------------------------------------------ + # Cancel + # ------------------------------------------------------------------ + + def cancel_order(self, client_order_id: str) -> ExecutionReport: + with self._lock: + broker_id = self._client_to_broker.get(client_order_id) + + if broker_id is None: + return ExecutionReport( + execution_id=f"cancel-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=None, + status=ExecutionStatus.FAILED, + symbol="UNKNOWN", + side="", + quantity=0.0, + message="Broker order ID not found for client order ID.", + ) + + open_trades = {t.order.orderId: t for t in self._ib.openTrades()} + trade = open_trades.get(broker_id) + if trade: + self._ib.cancelOrder(trade.order) + + report = ExecutionReport( + execution_id=f"cancel-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=str(broker_id), + status=ExecutionStatus.CANCELLED, + symbol=self._open_orders.get(client_order_id, OpenOrder( + client_order_id=client_order_id, symbol="UNKNOWN", + side="", quantity=0.0, order_type="", status="", + )).symbol, + side="", + quantity=0.0, + message="Cancel request sent to IBKR.", + updated_at_utc=utc_now(), + ) + + if self._journal: + self._journal.write_report(report) + + self.on_cancel.emit({"event": "cancel", "client_order_id": client_order_id, "report": to_jsonable(report)}) + return report + + # ------------------------------------------------------------------ + # Query + # ------------------------------------------------------------------ + + def get_open_orders(self) -> List[OpenOrder]: + trades = self._ib.openTrades() + result: List[OpenOrder] = [] + for trade in trades: + o = trade.order + c = trade.contract + result.append( + OpenOrder( + client_order_id=str(o.orderId), + symbol=c.symbol, + side=o.action, + quantity=float(o.totalQuantity), + order_type=o.orderType, + status=trade.orderStatus.status, + broker_order_id=str(o.orderId), + limit_price=o.lmtPrice if o.lmtPrice else None, + stop_price=o.auxPrice if o.auxPrice else None, + ) + ) + return result + + def get_positions(self) -> List[PositionSnapshot]: + positions = self._ib.positions() + result: List[PositionSnapshot] = [] + for pos in positions: + result.append( + PositionSnapshot( + symbol=pos.contract.symbol, + quantity=float(pos.position), + avg_cost=float(pos.avgCost), + account=pos.account, + ) + ) + return result + + def get_account(self) -> AccountSnapshot: + summary = self._ib.accountSummary() + values: Dict[str, float] = {} + for item in summary: + try: + values[item.tag] = float(item.value) + except (ValueError, TypeError): + pass + + return AccountSnapshot( + account_id=summary[0].account if summary else "IBKR", + net_liquidation=values.get("NetLiquidation", 0.0), + available_funds=values.get("AvailableFunds", 0.0), + buying_power=values.get("BuyingPower", 0.0), + excess_liquidity=values.get("ExcessLiquidity", 0.0), + realized_pnl=values.get("RealizedPnL", 0.0), + unrealized_pnl=values.get("UnrealizedPnL", 0.0), + ) + + def health_check(self) -> Dict[str, Any]: + connected = self._ib.isConnected() + return { + "broker": "IBKRBroker", + "connected": connected, + "host": self._host, + "port": self._port, + "client_id": self._client_id, + "environment": self._environment.value, + "timestamp_utc": utc_now(), + } + + # ------------------------------------------------------------------ + # IBKR event handlers + # ------------------------------------------------------------------ + + def _on_order_status(self, trade: Any) -> None: + status_text = trade.orderStatus.status + status = normalize_status(status_text) + client_order_id = str(trade.order.orderId) + + with self._lock: + if client_order_id in self._open_orders: + self._open_orders[client_order_id].status = status.value + + report = ExecutionReport( + execution_id=f"status-{uuid.uuid4().hex[:12]}", + client_order_id=client_order_id, + broker_order_id=str(trade.order.orderId), + status=status, + symbol=trade.contract.symbol, + side=trade.order.action, + quantity=float(trade.order.totalQuantity), + filled_quantity=float(trade.orderStatus.filled), + avg_fill_price=float(trade.orderStatus.avgFillPrice) if trade.orderStatus.avgFillPrice else None, + message=status_text, + updated_at_utc=utc_now(), + ) + + if self._journal: + self._journal.write_report(report) + + self.on_status_change.emit({"event": "status_change", "status": status.value, "report": to_jsonable(report)}) + + if status == ExecutionStatus.FILLED: + self.on_fill.emit({"event": "fill", "report": to_jsonable(report)}) + elif status == ExecutionStatus.CANCELLED: + self.on_cancel.emit({"event": "cancel", "report": to_jsonable(report)}) + + def _on_error(self, req_id: int, error_code: int, error_string: str, contract: Any) -> None: + logger.error( + "IBKR error: reqId=%d code=%d msg=%s contract=%s", + req_id, error_code, error_string, contract, + ) + + # ------------------------------------------------------------------ + # Contract / order builders + # ------------------------------------------------------------------ + + def _build_contract(self, instrument: Instrument) -> Any: + asset = instrument.asset_type + if asset == AssetType.STOCK: + return Stock( + instrument.symbol, + instrument.exchange, + instrument.currency, + ) + if asset == AssetType.FOREX: + return Forex(instrument.symbol) + if asset == AssetType.FUTURE: + return Future( + symbol=instrument.symbol, + lastTradeDateOrContractMonth=instrument.expiry or "", + exchange=instrument.exchange, + currency=instrument.currency, + multiplier=instrument.multiplier or "", + ) + if asset == AssetType.CRYPTO: + return Crypto( + symbol=instrument.symbol, + exchange=instrument.exchange, + currency=instrument.currency, + ) + raise ValueError(f"Unsupported asset type for IBKR: {asset}") + + def _build_ib_order(self, intent: OrderIntent, quantity: float) -> Any: + action = intent.side.value + tif = intent.tif.value + + if intent.order_type == OrderType.MARKET: + return MarketOrder(action=action, totalQuantity=quantity, tif=tif) + if intent.order_type == OrderType.LIMIT: + return LimitOrder(action=action, totalQuantity=quantity, lmtPrice=intent.limit_price, tif=tif) + if intent.order_type == OrderType.STOP: + return StopOrder(action=action, totalQuantity=quantity, stopPrice=intent.stop_price, tif=tif) + if intent.order_type == OrderType.STOP_LIMIT: + return StopLimitOrder( + action=action, + totalQuantity=quantity, + stopPrice=intent.stop_price, + lmtPrice=intent.limit_price, + tif=tif, + ) + raise ValueError(f"Unsupported order type: {intent.order_type}") + + def _rejected_report(self, intent: OrderIntent, message: str) -> ExecutionReport: + return ExecutionReport( + execution_id=f"exec-{uuid.uuid4().hex[:12]}", + client_order_id=intent.client_order_id, + broker_order_id=None, + status=ExecutionStatus.REJECTED, + symbol=intent.instrument.symbol, + side=intent.side.value, + quantity=intent.quantity, + message=message, + ) + + def _emit_reject(self, report: ExecutionReport, reason: str) -> None: + self.on_reject.emit({"event": "reject", "reason": reason, "report": to_jsonable(report)}) + + +# ============================================================================= +# BROKER FACTORY +# ============================================================================= + +class BrokerFactory: + """ + Constructs and returns the appropriate broker adapter based on the + requested environment or explicit broker type. + + Examples + -------- + >>> broker = BrokerFactory.create(environment=Environment.PAPER) + >>> broker.connect() + >>> report = broker.submit_order(intent) + >>> broker.disconnect() + """ + + @staticmethod + def create( + environment: Environment = Environment.PAPER, + broker_type: Optional[str] = None, + journal: Optional[ExecutionJournal] = None, + risk_engine: Optional[RiskEngine] = None, + **kwargs: Any, + ) -> BrokerAdapter: + """ + Parameters + ---------- + environment: + PAPER for simulation, LIVE for real orders. + broker_type: + Optional override: 'paper' or 'ibkr'. + journal: + Shared ExecutionJournal instance for persistence. + risk_engine: + Shared RiskEngine instance. + **kwargs: + Forwarded to the chosen broker adapter's constructor. + """ + resolved = (broker_type or environment.value).lower() + + if resolved == "paper": + return PaperBroker( + journal=journal, + risk_engine=risk_engine, + **kwargs, + ) + + if resolved in ("ibkr", "live"): + return IBKRBroker( + environment=environment, + journal=journal, + risk_engine=risk_engine, + **kwargs, + ) + + raise ValueError(f"Unknown broker type: {resolved!r}. Choose 'paper' or 'ibkr'.") + + +# ============================================================================= +# MODULE SELF-TEST +# ============================================================================= + +if __name__ == "__main__": + import tempfile + + print("=== ALICE Broker Adapter - Self Test ===\n") + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + db_path = f.name + + journal = ExecutionJournal(db_path) + risk_engine = RiskEngine(journal) + + broker = BrokerFactory.create( + environment=Environment.PAPER, + journal=journal, + risk_engine=risk_engine, + initial_cash=50_000.0, + ) + broker.connect() + + fills: List[Dict[str, Any]] = [] + broker.on_fill.subscribe(lambda ev: fills.append(ev)) + + # Feed a price + broker.update_price("AAPL", 175.0) # type: ignore[attr-defined] + + # Build risk envelope + risk = RiskEnvelope( + max_notional=10_000.0, + max_position_qty=100.0, + max_daily_loss=2_000.0, + max_symbol_exposure=200.0, + max_order_risk=1_000.0, + min_confidence=0.5, + allow_short=True, + ) + + decision = DecisionContext( + source=IntentSource.ALICE, + strategy_id="demo", + strategy_version="1.0", + confidence=0.85, + ) + + # --- Test 1: Simple market buy --- + intent = OrderIntent( + instrument=Instrument("AAPL"), + action=DecisionAction.ENTER_LONG, + side=OrderSide.BUY, + quantity=10.0, + risk=risk, + decision=decision, + ) + report = broker.submit_order(intent) + print(f"[BUY] status={report.status.value} fill_price={report.avg_fill_price}") + assert report.status == ExecutionStatus.FILLED, f"Expected FILLED got {report.status}" + assert len(fills) == 1 + + # --- Test 2: Market sell --- + intent2 = OrderIntent( + instrument=Instrument("AAPL"), + action=DecisionAction.EXIT_LONG, + side=OrderSide.SELL, + quantity=10.0, + risk=risk, + decision=decision, + ) + report2 = broker.submit_order(intent2) + print(f"[SELL] status={report2.status.value} fill_price={report2.avg_fill_price}") + assert report2.status == ExecutionStatus.FILLED + + # --- Test 3: Bracket order --- + broker.update_price("AAPL", 175.0) # type: ignore[attr-defined] + bracket_intent = OrderIntent( + instrument=Instrument("AAPL"), + action=DecisionAction.ENTER_LONG, + side=OrderSide.BUY, + quantity=5.0, + stop_price=170.0, + take_profit_price=185.0, + risk=risk, + decision=decision, + ) + bracket_result = broker.submit_bracket(bracket_intent) + print(f"[BRACKET] parent={bracket_result.parent.status.value}") + assert bracket_result.parent.status == ExecutionStatus.FILLED + + # --- Test 4: Confidence rejection --- + low_conf_decision = DecisionContext( + source=IntentSource.ALICE, + strategy_id="demo", + strategy_version="1.0", + confidence=0.1, + ) + intent3 = OrderIntent( + instrument=Instrument("AAPL"), + action=DecisionAction.ENTER_LONG, + side=OrderSide.BUY, + quantity=5.0, + risk=risk, + decision=low_conf_decision, + ) + report3 = broker.submit_order(intent3) + print(f"[LOW CONF] status={report3.status.value}") + assert report3.status == ExecutionStatus.REJECTED + + # --- Test 5: Cancel a limit order --- + limit_intent = OrderIntent( + instrument=Instrument("AAPL"), + action=DecisionAction.ENTER_LONG, + side=OrderSide.BUY, + quantity=3.0, + order_type=OrderType.LIMIT, + limit_price=160.0, + risk=risk, + decision=decision, + ) + limit_report = broker.submit_order(limit_intent) + cancel_report = broker.cancel_order(limit_intent.client_order_id) + print(f"[CANCEL] status={cancel_report.status.value}") + assert cancel_report.status == ExecutionStatus.CANCELLED + + # --- Account snapshot --- + acct = broker.get_account() + print(f"\nAccount: cash={acct.available_funds:.2f} realized_pnl={acct.realized_pnl:.2f}") + + # --- Health check --- + health = broker.health_check() + print(f"Health: {health}") + + broker.disconnect() + print("\n=== All assertions passed ===") From 200b40ce9fed5b332668234754d5cdcf0f878636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:16:43 +0000 Subject: [PATCH 3/4] Add .gitignore and remove committed pycache Agent-Logs-Url: https://github.com/Netrade1/Institutional-Microstructure-/sessions/0f9d3c6e-b4ef-4aa1-88d8-7532473ed78d Co-authored-by: Netrade1 <146481409+Netrade1@users.noreply.github.com> --- .gitignore | 13 +++++++++++++ __pycache__/alice_broker.cpython-312.pyc | Bin 85266 -> 0 bytes 2 files changed, 13 insertions(+) create mode 100644 .gitignore delete mode 100644 __pycache__/alice_broker.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29d301e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.db +*.db-wal +*.db-shm +.env +.venv/ +venv/ +dist/ +build/ +*.egg-info/ diff --git a/__pycache__/alice_broker.cpython-312.pyc b/__pycache__/alice_broker.cpython-312.pyc deleted file mode 100644 index 3cfcd9d93c7f40ba41efe04355b9dbfa10c6c438..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85266 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

L&+(7 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 NFOXYoJzSOke*tzHWIg}@ From 7488fdf92b3c8392375d0ce65041cae16e949baf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:18:15 +0000 Subject: [PATCH 4/4] Fix unreachable code and realized PnL for short-cover fills Agent-Logs-Url: https://github.com/Netrade1/Institutional-Microstructure-/sessions/0f9d3c6e-b4ef-4aa1-88d8-7532473ed78d Co-authored-by: Netrade1 <146481409+Netrade1@users.noreply.github.com> --- alice_broker.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/alice_broker.py b/alice_broker.py index 361dbe6..5df2746 100644 --- a/alice_broker.py +++ b/alice_broker.py @@ -788,14 +788,11 @@ def apply_fill(self, side: OrderSide, qty: float, fill_price: float) -> float: # Opening or adding to short if self.quantity == 0: self.avg_cost = fill_price + self.quantity -= qty else: total_cost = self.avg_cost * abs(self.quantity) + fill_price * qty self.quantity -= qty self.avg_cost = total_cost / abs(self.quantity) if self.quantity != 0 else fill_price - qty = 0 - if qty > 0: - self.quantity -= qty - self.avg_cost = fill_price self.realized_pnl += realized return realized @@ -1075,7 +1072,7 @@ def _execute_fill(self, client_order_id: str, fill_price: float) -> Optional[Exe "side": order.side, "qty": qty, "fill_price": fill_price, - "realized_pnl": realized if order.side == OrderSide.SELL.value else 0.0, + "realized_pnl": realized, "report": to_jsonable(report), } )