Implement Binance-ready automated trading bot runtime - #17
Conversation
Task DeliverablesGenerated by Ascii Automated Binance-ready bot runtimeImplemented a full trading-bot path in
flowchart LR
A[Binance/CCXT market data] --> B[Indicator enrichment]
B --> C[Diffusion inference]
C --> D[Signal contract]
D --> E[Risk engine]
E -->|approved| F[Paper or Binance execution]
E -->|rejected| G[Event log]
F --> H[SQLite persistence]
G --> H
H --> I[Monitoring hooks]
flowchart TD
A[Operator] -->|pause| B[runtime/control/pause]
A -->|kill| C[runtime/control/kill]
D[Risk breach] --> C
B --> E[Bot skips iteration]
C --> F[Bot refuses to trade]
|
| self.cash_usd = config.initial_balance_usd | ||
| self.start_equity_usd = config.initial_balance_usd | ||
| self.daily_start_equity_usd = config.initial_balance_usd | ||
| self.daily_key = self.risk.utc_day_key() | ||
| self.consecutive_losses = 0 | ||
| self.position = self._load_or_create_position() |
There was a problem hiding this comment.
🔴 Cash balance not persisted, causing inflated equity after bot restart with open position
When the bot restarts, cash_usd is reset to config.initial_balance_usd (line 47) while self.position is loaded from the SQLite database at line 52 via _load_or_create_position. If the previous run had an open position (e.g., a long purchased for $2,000), the cash should have been reduced by that amount, but it resets to the full initial balance (e.g., $10,000). The bot then computes equity as cash_usd + unrealized (bot/runtime.py:89), which would be $10,000 + unrealized instead of the correct $8,000 + unrealized. This inflated equity flows into the risk engine's position sizing at bot/risk_engine.py:52-53, allowing oversized positions. Similarly, start_equity_usd, daily_start_equity_usd, and consecutive_losses are not persisted, meaning drawdown guards and loss streak limits are also reset on restart.
Prompt for agents
The core problem is that bot/runtime.py initializes cash_usd, start_equity_usd, daily_start_equity_usd, and consecutive_losses from config defaults at lines 47-51, but then loads the position from SQLite at line 52 via _load_or_create_position. If a position already exists in the DB (from a previous run), the cash and equity baselines are inconsistent with the loaded position.
To fix this, the bot's portfolio state (cash_usd, start_equity_usd, daily_start_equity_usd, consecutive_losses) should also be persisted in SQLite. This likely requires:
1. A new table in bot/persistence.py (e.g., portfolio_state) with columns for cash_usd, start_equity_usd, daily_start_equity_usd, daily_key, consecutive_losses.
2. A save_portfolio_state and get_portfolio_state method in Persistence.
3. In TradingBot.__init__, after loading the position, also load the portfolio state from DB. If none exists, use the config defaults.
4. In run_once and _update_position_from_fill, persist the portfolio state alongside the position.
Alternatively, a simpler approach would be to reconstruct cash_usd from the position and order history in the DB, but this is more complex. The persistence approach is cleaner.
Was this helpful? React with 👍 or 👎 to provide feedback.
| elif intent == "close_long": | ||
| realized_change = (price - self.position.entry_price) * self.position.quantity - fees | ||
| self.cash_usd += self.position.quantity * price - fees | ||
| self.position.realized_pnl += realized_change | ||
| self.position.quantity = 0.0 | ||
| self.position.entry_price = 0.0 | ||
| elif intent == "open_short": | ||
| self.cash_usd += qty * price - fees | ||
| self.position.quantity = -qty | ||
| self.position.entry_price = price | ||
| elif intent == "close_short": | ||
| realized_change = (self.position.entry_price - price) * abs(self.position.quantity) - fees | ||
| self.cash_usd -= abs(self.position.quantity) * price + fees | ||
| self.position.realized_pnl += realized_change | ||
| self.position.quantity = 0.0 | ||
| self.position.entry_price = 0.0 |
There was a problem hiding this comment.
🟡 Close handlers use self.position.quantity instead of actual fill quantity, ignoring partial fills
In _update_position_from_fill, the variable qty = fill.quantity is assigned at line 92 and correctly used in the open_long and open_short handlers. However, the close_long (lines 101-106) and close_short (lines 111-116) handlers use self.position.quantity instead of qty for cash and PnL calculations. If the exchange returns a partial fill (where fill.quantity < self.position.quantity), the bot credits/debits cash for the full position size, then zeroes out the position — creating an accounting mismatch with the actual exchange state. While partial fills are rare for market orders, this is architecturally incorrect and would cause real financial discrepancies in Binance live/testnet modes.
| elif intent == "close_long": | |
| realized_change = (price - self.position.entry_price) * self.position.quantity - fees | |
| self.cash_usd += self.position.quantity * price - fees | |
| self.position.realized_pnl += realized_change | |
| self.position.quantity = 0.0 | |
| self.position.entry_price = 0.0 | |
| elif intent == "open_short": | |
| self.cash_usd += qty * price - fees | |
| self.position.quantity = -qty | |
| self.position.entry_price = price | |
| elif intent == "close_short": | |
| realized_change = (self.position.entry_price - price) * abs(self.position.quantity) - fees | |
| self.cash_usd -= abs(self.position.quantity) * price + fees | |
| self.position.realized_pnl += realized_change | |
| self.position.quantity = 0.0 | |
| self.position.entry_price = 0.0 | |
| elif intent == "close_long": | |
| realized_change = (price - self.position.entry_price) * qty - fees | |
| self.cash_usd += qty * price - fees | |
| self.position.realized_pnl += realized_change | |
| self.position.quantity -= qty | |
| if self.position.quantity <= 0: | |
| self.position.quantity = 0.0 | |
| self.position.entry_price = 0.0 | |
| elif intent == "open_short": | |
| self.cash_usd += qty * price - fees | |
| self.position.quantity = -qty | |
| self.position.entry_price = price | |
| elif intent == "close_short": | |
| realized_change = (self.position.entry_price - price) * qty - fees | |
| self.cash_usd -= qty * price + fees | |
| self.position.realized_pnl += realized_change | |
| self.position.quantity += qty | |
| if self.position.quantity >= 0: | |
| self.position.quantity = 0.0 | |
| self.position.entry_price = 0.0 |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Test plan