|
@@ -0,0 +1,205 @@
|
|
|
+# Trade Lifecycle Management
|
|
|
+
|
|
|
+This document explains how the trading bot manages the lifecycle of a trade, from its initiation to its conclusion, including various states, updates, and handling of special conditions like stop losses and external exchange activities.
|
|
|
+
|
|
|
+## Core Components
|
|
|
+
|
|
|
+The trade lifecycle is primarily managed by the interplay of three core components:
|
|
|
+
|
|
|
+1. **`src/trading/trading_engine.py`**: Responsible for initiating trade actions (placing entry, exit, stop-loss orders) and interacting with the exchange client. It relies on `TradingStats` for state persistence.
|
|
|
+2. **`src/trading/trading_stats.py`**: The brain for state management. It uses an SQLite database (`data/trading_stats.sqlite`) to store and update the status of orders and trade lifecycles. Key tables are `orders` and `trades`.
|
|
|
+3. **`src/monitoring/market_monitor.py`**: Continuously monitors the exchange for events like order fills, cancellations, and external activities. It updates the state in `TradingStats` based on real-world exchange events.
|
|
|
+
|
|
|
+## Database Schema for Lifecycle Tracking
|
|
|
+
|
|
|
+Two main tables in `trading_stats.sqlite` are central to lifecycle management:
|
|
|
+
|
|
|
+### 1. `orders` Table
|
|
|
+Stores information about individual orders placed by the bot or detected.
|
|
|
+Key fields:
|
|
|
+* `id` (INTEGER, PK): Database ID for the order.
|
|
|
+* `bot_order_ref_id` (TEXT, UNIQUE): A UUID generated by the bot for internal tracking *before* an exchange ID is available.
|
|
|
+* `exchange_order_id` (TEXT, UNIQUE): The order ID provided by the exchange.
|
|
|
+* `symbol` (TEXT): Trading pair (e.g., "ETH/USDC:USDC").
|
|
|
+* `side` (TEXT): 'buy' or 'sell'.
|
|
|
+* `type` (TEXT): Order type (e.g., 'market', 'limit', 'stopMarket', 'stopLimit', 'stop_loss_trigger').
|
|
|
+* `status` (TEXT): Critical for lifecycle. Examples:
|
|
|
+ * `pending_submission`: Bot intends to submit, not yet confirmed by exchange.
|
|
|
+ * `open`: Actively working on the exchange.
|
|
|
+ * `filled`: Order fully executed.
|
|
|
+ * `partially_filled`: Order partially executed.
|
|
|
+ * `cancelled`: Order cancelled (by bot, user, or system).
|
|
|
+ * `rejected`: Exchange rejected the order.
|
|
|
+ * `expired`: Order expired.
|
|
|
+ * `pending_trigger`: A conditional order (like a stop loss) whose trigger conditions have not yet been met.
|
|
|
+ * `failed_submission`: Bot failed to submit the order to the exchange.
|
|
|
+* `parent_bot_order_ref_id` (TEXT, NULLABLE): Links conditional orders (like SL triggers) to their parent entry order. This helps maintain context, especially for stop losses attached to a specific trade.
|
|
|
+* `timestamp_created` (TEXT), `timestamp_updated` (TEXT).
|
|
|
+
|
|
|
+### 2. `trades` Table
|
|
|
+Stores information about entire trade lifecycles, which can consist of one or more orders (e.g., entry, TP, SL).
|
|
|
+Key fields:
|
|
|
+* `id` (INTEGER, PK): Database ID for the trade record.
|
|
|
+* `trade_lifecycle_id` (TEXT, UNIQUE): A UUID grouping all related activities (entry, exit, adjustments) for a single conceptual trade.
|
|
|
+* `symbol` (TEXT).
|
|
|
+* `status` (TEXT): Critical for lifecycle. Examples:
|
|
|
+ * `pending`: Lifecycle initiated, entry order may be `pending_submission` or `open`.
|
|
|
+ * `executed`: Entry order has been placed (usually for market orders that are expected to fill quickly).
|
|
|
+ * `position_opened`: Entry order confirmed filled, position is active on the exchange.
|
|
|
+ * `position_closed`: Position has been fully closed (via TP, SL, or manual exit).
|
|
|
+ * `cancelled`: The trade was cancelled before a position was opened, or an open position's management was explicitly cancelled.
|
|
|
+* `position_side` (TEXT): 'long', 'short'. Becomes 'flat' implicitly when `position_closed`.
|
|
|
+* `entry_price` (REAL), `current_position_size` (REAL).
|
|
|
+* `stop_loss_price` (REAL), `take_profit_price` (REAL).
|
|
|
+* `entry_order_id` (TEXT), `stop_loss_order_id` (TEXT), `take_profit_order_id` (TEXT): Exchange order IDs for the respective parts of the trade.
|
|
|
+* `realized_pnl` (REAL), `unrealized_pnl` (REAL).
|
|
|
+* `position_opened_at` (TEXT), `position_closed_at` (TEXT).
|
|
|
+* `linked_order_table_id` (INTEGER): Foreign key to the `orders` table, linking a specific fill event to this trade record.
|
|
|
+
|
|
|
+## Trade Lifecycle Stages & Status Transitions
|
|
|
+
|
|
|
+Here's a typical flow of a trade and how its status evolves:
|
|
|
+
|
|
|
+**1. Initiation (e.g., User sends `/long ETH 100 sl 1800`)**
|
|
|
+
|
|
|
+* **`TradingEngine`**:
|
|
|
+ * Generates a unique `bot_order_ref_id` for the entry order.
|
|
|
+ * Calls `TradingStats.record_order_placed()`:
|
|
|
+ * An entry is made in the `orders` table.
|
|
|
+ * `orders.status` = `pending_submission`.
|
|
|
+ * `orders.bot_order_ref_id` is set.
|
|
|
+ * If a stop loss is part of the initial command (`stop_loss_price` provided):
|
|
|
+ * A separate order may be recorded in `orders` with `type='stop_loss_trigger'`, `status='pending_trigger'`, and `parent_bot_order_ref_id` linking it to the entry order's `bot_order_ref_id`.
|
|
|
+ * Alternatively, the stop loss might be implicitly tracked against the trade lifecycle.
|
|
|
+ * Calls `TradingStats.create_trade_lifecycle()`:
|
|
|
+ * An entry is made in the `trades` table.
|
|
|
+ * `trades.trade_lifecycle_id` is generated.
|
|
|
+ * `trades.status` = `pending` (or `executed` if it's a market order).
|
|
|
+ * `trades.symbol`, `trades.position_side` are set.
|
|
|
+ * `trades.stop_loss_price` is recorded if provided.
|
|
|
+
|
|
|
+**2. Order Submission to Exchange**
|
|
|
+
|
|
|
+* **`TradingEngine`**:
|
|
|
+ * Attempts to place the entry order via `hyperliquid_client`.
|
|
|
+ * **On Success**:
|
|
|
+ * Receives `exchange_order_id` from the exchange.
|
|
|
+ * Calls `TradingStats.update_order_status()`:
|
|
|
+ * `orders.status` updated to `open` (for limit orders) or a transient status like `submitted_market` (market orders await fill confirmation from `MarketMonitor`).
|
|
|
+ * `orders.exchange_order_id` is populated.
|
|
|
+ * The `trades` table entry (created in step 1) remains `pending` or `executed`.
|
|
|
+ * **On Failure (e.g., insufficient funds, API error)**:
|
|
|
+ * Calls `TradingStats.update_order_status()`:
|
|
|
+ * `orders.status` updated to `failed_submission` or `failed_submission_no_data`.
|
|
|
+ * Calls `TradingStats.update_trade_cancelled()`:
|
|
|
+ * `trades.status` updated to `cancelled`.
|
|
|
+ * The lifecycle ends here.
|
|
|
+
|
|
|
+**3. Exchange Acknowledges Order (Order is Live)**
|
|
|
+
|
|
|
+* **`MarketMonitor`**:
|
|
|
+ * In its periodic `_update_cached_data()` and `_check_order_fills()` loop, it sees the new order (with `exchange_order_id`) as open on the exchange.
|
|
|
+ * The `orders.status` is typically `open`.
|
|
|
+
|
|
|
+**4. Entry Order Fill Detection**
|
|
|
+
|
|
|
+* **`MarketMonitor` (`_check_order_fills`, `_process_filled_orders`)**:
|
|
|
+ * Detects that the entry `exchange_order_id` is no longer open (implying it was filled or cancelled).
|
|
|
+ * It verifies against recent fills from the exchange.
|
|
|
+ * Assuming it was filled:
|
|
|
+ * Calls `TradingStats.update_order_status()`:
|
|
|
+ * `orders.status` (for the entry order) updated to `filled`.
|
|
|
+ * `orders.amount_filled` is updated.
|
|
|
+ * Calls `TradingStats.update_trade_position_opened()`:
|
|
|
+ * `trades.status` updated to `position_opened`.
|
|
|
+ * `trades.entry_price`, `trades.current_position_size`, `trades.position_opened_at` are recorded.
|
|
|
+ * `trades.entry_order_id` is set to the `exchange_order_id` of the filled entry order.
|
|
|
+ * If a stop loss was pre-defined (e.g., `stop_loss_price` in the `trades` table), and a `pending_trigger` SL order exists in the `orders` table, it remains in that state, now actively monitored against the open position.
|
|
|
+
|
|
|
+**5. Position is Open and Managed**
|
|
|
+
|
|
|
+* The trade is now live (`trades.status = 'position_opened'`).
|
|
|
+* `MarketMonitor` continues its checks:
|
|
|
+ * **Pending Stop Loss Monitoring (`_activate_pending_stop_losses_from_trades`, `_check_pending_triggers`)**:
|
|
|
+ * If a `stop_loss_price` is defined in the `trades` table for this `position_opened` lifecycle (or a linked `pending_trigger` order exists in the `orders` table).
|
|
|
+ * `MarketMonitor` checks the current market price against this `stop_loss_price`.
|
|
|
+ * **If Triggered**:
|
|
|
+ * `MarketMonitor` instructs `TradingEngine.execute_triggered_stop_order()` (or a similar method).
|
|
|
+ * `TradingEngine` places a *new* market or limit stop order on the exchange.
|
|
|
+ * This new SL order is recorded in the `orders` table by `TradingEngine` (via `TradingStats.record_order_placed()`), likely with `status='open'` and `type='stopMarket'` or `'stopLimit'`.
|
|
|
+ * `TradingStats.link_stop_loss_to_trade()` is called to update `trades.stop_loss_order_id` with the `exchange_order_id` of this newly placed active SL order.
|
|
|
+ * The original `pending_trigger` order (if one existed) in the `orders` table is updated to `triggered` or `cancelled_by_activation`.
|
|
|
+
|
|
|
+**6. Position Closure (SL, TP, or Manual Exit)**
|
|
|
+
|
|
|
+* **Scenario A: Stop Loss Order Fills**
|
|
|
+ * `MarketMonitor` detects the SL `exchange_order_id` (from `trades.stop_loss_order_id`) has filled.
|
|
|
+ * Calls `TradingStats.update_order_status()` for the SL order: `orders.status` = `filled`.
|
|
|
+ * Calls `TradingStats.update_trade_position_closed()`:
|
|
|
+ * `trades.status` = `position_closed`.
|
|
|
+ * `trades.realized_pnl`, `trades.position_closed_at`, and exit price details are recorded.
|
|
|
+ * If a Take Profit order was also active for this lifecycle, `TradingStats.cancel_linked_orders()` is called to cancel it. The TP order's status in `orders` becomes `cancelled_linked_fill` (or similar).
|
|
|
+* **Scenario B: Take Profit Order Fills**
|
|
|
+ * Similar to SL fill, but for the TP order. The linked SL order is cancelled.
|
|
|
+* **Scenario C: Manual Exit Command (e.g., `/exit ETH`)**
|
|
|
+ * `TradingCommands` calls `TradingEngine.execute_exit_order()`.
|
|
|
+ * `TradingEngine` places a market order to close the position. This new exit order goes through the `pending_submission` -> `open` (if limit) / `submitted_market` -> `filled` flow in the `orders` table.
|
|
|
+ * When `MarketMonitor` detects this exit order fill:
|
|
|
+ * Calls `TradingStats.update_trade_position_closed()`: `trades.status` = `position_closed`.
|
|
|
+ * `TradingStats.cancel_pending_stop_losses_by_symbol()` or `cancel_linked_orders()` is called to cancel any active SL/TP orders associated with this `trade_lifecycle_id`. Their status in `orders` becomes `cancelled_position_closed` (or similar).
|
|
|
+
|
|
|
+**7. Order Cancellation**
|
|
|
+
|
|
|
+Orders can be cancelled at various stages:
|
|
|
+
|
|
|
+* **By Bot User (e.g., `/coo` command for "Cancel All Orders" for a symbol)**:
|
|
|
+ * `TradingCommands` calls `TradingEngine.cancel_all_orders()`.
|
|
|
+ * `TradingEngine` calls `client.cancel_order()` for each open order.
|
|
|
+ * `TradingStats.update_order_status()`: `orders.status` becomes `cancelled`.
|
|
|
+ * If an entry order is cancelled before opening a position, `TradingStats.update_trade_cancelled()` sets `trades.status` to `cancelled`.
|
|
|
+* **By System (e.g., Orphaned Stop Loss)**:
|
|
|
+ * `MarketMonitor._cleanup_orphaned_stop_losses()`: If a position (`trades.status = 'position_closed' or 'cancelled'`) has a lingering SL order that is still `open` or `pending_trigger`.
|
|
|
+ * `MarketMonitor` instructs `TradingEngine` to cancel it on the exchange.
|
|
|
+ * `TradingStats.update_order_status()` for the SL order: `orders.status` = `cancelled_orphaned`.
|
|
|
+* **Detected by `MarketMonitor` (e.g., user cancelled on exchange interface)**:
|
|
|
+ * `MarketMonitor._process_disappeared_orders()`: If an order known to the bot vanishes from the exchange without a fill.
|
|
|
+ * After checks, if determined to be a cancellation:
|
|
|
+ * `TradingStats.update_order_status()`: `orders.status` = `cancelled_external` (or a similar status indicating external action).
|
|
|
+ * If this was a critical entry order, `trades.status` might also become `cancelled`.
|
|
|
+
|
|
|
+## Handling External Trades/Positions (Outside Telegram/Bot Control)
|
|
|
+
|
|
|
+The system is designed to detect and adapt to trading activity that occurs directly on the exchange, not initiated by the bot.
|
|
|
+
|
|
|
+* **External Fill Detection (`MarketMonitor._check_external_trades`)**:
|
|
|
+ * `MarketMonitor` fetches recent exchange fills.
|
|
|
+ * It compares these fills with orders known to the bot (via `bot_order_ref_id` or `exchange_order_id` in the `orders` table).
|
|
|
+ * **If a fill does not match any bot order**:
|
|
|
+ * It's treated as an external trade.
|
|
|
+ * `TradingStats.record_trade()` is used to log this activity. This might:
|
|
|
+ * Create a new entry in the `trades` table if it opens a new position. The `status` would become `position_opened`, and entry details are estimated from the fill.
|
|
|
+ * Update an existing `trades` entry if the external fill closes or modifies a bot-known position. For example, if the bot has `trades.status = 'position_opened'` for ETH, and an external sell fill for ETH is detected, `TradingStats.update_trade_position_closed()` would be called.
|
|
|
+* **Orphaned Position Sync (`MarketMonitor._auto_sync_orphaned_positions`)**:
|
|
|
+ * **Scenario 1: Exchange has position, Bot doesn't know.**
|
|
|
+ * `MarketMonitor` sees a position on the exchange that doesn't correspond to any `position_opened` lifecycle in `trades` table.
|
|
|
+ * It calls `TradingStats.create_trade_lifecycle()` and `TradingStats.update_trade_position_opened()` to create a new record in `trades`, marking it as `position_opened`. Entry price is estimated from recent fills if possible. This ensures the bot is aware of the position for potential future management or correct P&L.
|
|
|
+ * **Scenario 2: Bot thinks position is open, Exchange doesn't.**
|
|
|
+ * `MarketMonitor` sees no corresponding position on the exchange for a `trades` entry that has `status = 'position_opened'`.
|
|
|
+ * It calls `TradingStats.update_trade_position_closed()`, assuming the position was closed externally. Details like exit price might be approximate or marked as "external close."
|
|
|
+
|
|
|
+## Risk Management Integration
|
|
|
+
|
|
|
+While the detailed logic for `_check_automatic_risk_management` in `MarketMonitor` isn't fully elaborated from the snippets, it would interact with this lifecycle:
|
|
|
+
|
|
|
+* **Modifying Orders**: If risk rules dictate adjusting a stop loss or taking profit, `MarketMonitor` would instruct `TradingEngine` to cancel the existing SL/TP order and place a new one. This involves:
|
|
|
+ * Cancelling the old order (updating its status in `orders` to `cancelled_risk_adjusted` or similar).
|
|
|
+ * Placing a new order (new entry in `orders` table, status `open`).
|
|
|
+ * Updating the relevant `trades` table fields (`stop_loss_price`, `stop_loss_order_id`, etc.).
|
|
|
+* **Preventing Trades**: If a global risk limit (e.g., max exposure) is hit, `TradingEngine` might be prevented from initiating new trades, or `MarketMonitor` might flag this. The `TradingEngine`'s order placement methods already include checks (e.g., for balance, valid prices) before attempting to submit orders.
|
|
|
+
|
|
|
+## Summary of Key Statuses
|
|
|
+
|
|
|
+* **`orders.status`**: `pending_submission`, `open`, `filled`, `cancelled` (with variants like `cancelled_external`, `cancelled_linked_fill`, `cancelled_orphaned`, `cancelled_position_closed`), `failed_submission`, `pending_trigger`, `triggered`.
|
|
|
+* **`trades.status`**: `pending`, `executed`, `position_opened`, `position_closed`, `cancelled`.
|
|
|
+
|
|
|
+This detailed flow ensures that the bot maintains a robust internal state of its trading activities, reconciles with actual exchange events, and handles various scenarios gracefully.
|