Initial commit — TSLA grid trading bot
- Grid strategy with survival-gated spacing and depth - Full 60% drop simulation for all survival checks - Calibration report with auto-updating survival threshold - Transaction history sync from Capital.com - Dip mode with bottom-two TP rules
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user