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.
The trade lifecycle is primarily managed by the interplay of three core components:
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.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
.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.Two main tables in trading_stats.sqlite
are central to lifecycle management:
orders
TableStores 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).trades
TableStores 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.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
:
bot_order_ref_id
for the entry order.TradingStats.record_order_placed()
:
orders
table.orders.status
= pending_submission
.orders.bot_order_ref_id
is set.stop_loss_price
provided):
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
.TradingStats.create_trade_lifecycle()
:
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
:
hyperliquid_client
.exchange_order_id
from the exchange.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.trades
table entry (created in step 1) remains pending
or executed
.TradingStats.update_order_status()
:
orders.status
updated to failed_submission
or failed_submission_no_data
.TradingStats.update_trade_cancelled()
:
trades.status
updated to cancelled
.3. Exchange Acknowledges Order (Order is Live)
MarketMonitor
:
_update_cached_data()
and _check_order_fills()
loop, it sees the new order (with exchange_order_id
) as open on the exchange.orders.status
is typically open
.4. Entry Order Fill Detection
MarketMonitor
(_check_order_fills
, _process_filled_orders
):
exchange_order_id
is no longer open (implying it was filled or cancelled).TradingStats.update_order_status()
:
orders.status
(for the entry order) updated to filled
.orders.amount_filled
is updated.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.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
trades.status = 'position_opened'
).MarketMonitor
continues its checks:
_activate_pending_stop_losses_from_trades
, _check_pending_triggers
):
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
.MarketMonitor
instructs TradingEngine.execute_triggered_stop_order()
(or a similar method).TradingEngine
places a new market or limit stop order on the exchange.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.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)
MarketMonitor
detects the SL exchange_order_id
(from trades.stop_loss_order_id
) has filled.TradingStats.update_order_status()
for the SL order: orders.status
= filled
.TradingStats.update_trade_position_closed()
:
trades.status
= position_closed
.trades.realized_pnl
, trades.position_closed_at
, and exit price details are recorded.TradingStats.cancel_linked_orders()
is called to cancel it. The TP order's status in orders
becomes cancelled_linked_fill
(or similar)./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.MarketMonitor
detects this exit order fill:
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:
/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
.TradingStats.update_trade_cancelled()
sets trades.status
to cancelled
.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
.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.TradingStats.update_order_status()
: orders.status
= cancelled_external
(or a similar status indicating external action).trades.status
might also become cancelled
.The system is designed to detect and adapt to trading activity that occurs directly on the exchange, not initiated by the bot.
MarketMonitor._check_external_trades
):
MarketMonitor
fetches recent exchange fills.bot_order_ref_id
or exchange_order_id
in the orders
table).TradingStats.record_trade()
is used to log this activity. This might:
trades
table if it opens a new position. The status
would become position_opened
, and entry details are estimated from the fill.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.MarketMonitor._auto_sync_orphaned_positions
):
MarketMonitor
sees a position on the exchange that doesn't correspond to any position_opened
lifecycle in trades
table.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.MarketMonitor
sees no corresponding position on the exchange for a trades
entry that has status = 'position_opened'
.TradingStats.update_trade_position_closed()
, assuming the position was closed externally. Details like exit price might be approximate or marked as "external close."While the detailed logic for _check_automatic_risk_management
in MarketMonitor
isn't fully elaborated from the snippets, it would interact with this lifecycle:
MarketMonitor
would instruct TradingEngine
to cancel the existing SL/TP order and place a new one. This involves:
orders
to cancelled_risk_adjusted
or similar).orders
table, status open
).trades
table fields (stop_loss_price
, stop_loss_order_id
, etc.).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.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.