""" state.py — Account state and position/order field helpers. =========================================================== Holds the current snapshot of the account, positions, and orders. Updated every loop from the API. All other modules read from here. FIELD ACCESS HELPERS: --------------------- Capital.com uses nested dicts with non-obvious field names. Rather than repeating o["workingOrderData"]["orderLevel"] everywhere, use the helper functions at the bottom of this file: pos_level(p) → float entry price of a position pos_size(p) → float size in shares pos_id(p) → deal ID string pos_tp(p) → take profit price (float) or None if not set pos_upl(p) → unrealised P&L in USD (float) ord_level(o) → float entry price of a working order ord_size(o) → float size in shares ord_id(o) → deal ID string """ import logging import config log = logging.getLogger("maxbot.state") # ───────────────────────────────────────────── # FIELD ACCESS HELPERS # Use these everywhere instead of raw dict access. # If Capital.com ever changes field names, fix it here only. # ───────────────────────────────────────────── def pos_level(p) -> float: return float(p["position"]["level"]) def pos_size(p) -> float: return float(p["position"]["size"]) def pos_id(p) -> str: return p["position"]["dealId"] def pos_tp(p): """Returns take profit price as float, or None if not set.""" tp = p["position"].get("profitLevel") return float(tp) if tp is not None else None def pos_upl(p) -> float: """Unrealised P&L in USD.""" return float(p["position"].get("upl", 0)) def ord_level(o) -> float: return float(o["workingOrderData"]["orderLevel"]) def ord_size(o) -> float: return float(o["workingOrderData"]["orderSize"]) def ord_id(o) -> str: return o["workingOrderData"]["dealId"] # ───────────────────────────────────────────── # BOT STATE # ───────────────────────────────────────────── class BotState: """ Snapshot of the account updated every loop. All values are recalculated from scratch each time fetch() is called. """ def __init__(self): self.positions: list = [] self.orders: list = [] self.equity: float = 0.0 # running equity in GBP self.funds: float = 0.0 # deposited funds in GBP self.available: float = 0.0 # available to trade in GBP self.current_price: float = 0.0 # TSLA mid price in USD self.gbpusd: float = config.GBPUSD_FALLBACK self.highest_open_price: float = 0.0 # highest open position entry self.lowest_level_price: float = 0.0 # lowest of positions OR orders self.total_margin_gbp: float = 0.0 # estimated margin in use self.margin_level_pct: float = 999.0 self.last_price: float = None # previous loop price (gap detection) def update(self, positions, orders, account, price_data, gbpusd): """Recalculate all state from fresh API data.""" self.positions = positions self.orders = orders self.gbpusd = gbpusd # Account balance for acc in account.get("accounts", []): if acc.get("preferred"): bal = acc.get("balance", {}) self.equity = float(bal.get("balance", 0)) self.funds = float(bal.get("deposit", 0)) self.available = float(bal.get("available", 0)) break # Current TSLA mid price snap = price_data.get("snapshot", {}) self.current_price = ( float(snap.get("bid", 0)) + float(snap.get("offer", 0)) ) / 2.0 # Highest open position entry price open_prices = [pos_level(p) for p in positions] self.highest_open_price = ( max(open_prices) if open_prices else self.current_price ) # Lowest level across all positions and orders order_prices = [ord_level(o) for o in orders] all_prices = open_prices + order_prices self.lowest_level_price = ( min(all_prices) if all_prices else self.current_price ) # Approximate total margin in use (GBP) # Formula: entry_price * size * margin_rate / gbpusd self.total_margin_gbp = sum( pos_level(p) * pos_size(p) * config.MARGIN_RATE / gbpusd for p in positions ) # Margin level % = equity / margin * 100 self.margin_level_pct = ( (self.equity / self.total_margin_gbp * 100) if self.total_margin_gbp > 0 else 999.0 ) def positions_sorted_asc(self) -> list: """Open positions sorted by entry price, lowest first.""" return sorted(self.positions, key=pos_level) def orders_sorted_asc(self) -> list: """Pending orders sorted by level, lowest first.""" return sorted(self.orders, key=ord_level) def all_levels(self) -> list: """ Combined list of all open positions and pending orders as dicts with 'price' and 'size' keys. Used by the survival check calculator. """ levels = [] for p in self.positions: levels.append({"price": pos_level(p), "size": pos_size(p)}) for o in self.orders: levels.append({"price": ord_level(o), "size": ord_size(o)}) return levels