architect_api/algo/
quote_one_side.rs

1use super::*;
2use crate::{
3    symbology::{ExecutionVenue, MarketdataVenue},
4    AccountIdOrName, Dir,
5};
6use anyhow::{bail, Result};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9
10/// An advanced algorithm that quotes one side of a market by joining the passive side within
11/// a specified number of ticks, with the option to improve the market by one tick to
12/// gain queue priority.
13///
14/// The primary intended use is in the context of spread trading, where it can be used
15/// to work the passive side of a spread while maintaining price competitiveness.
16///
17/// # Key Functionality
18///
19/// - `max_ticks_outside` determines the range of ticks from the best same-side price to quote.
20///   This is the maximum number of ticks outside (less aggressive than) the BBO that the algo will quote.
21///
22/// - This algo will always put out a limit order with a price that is equal to or less aggressive than the
23///   set limit price. It will attempt to only post liquidity, so it will not cross the market unless
24///   the market moves toward the order in the midst of sending the order.
25///
26/// - The algorithm can improve the market by one tick when:
27///   - `improve_or_join` is set to `Improve`
28///   - The opposite side is at least one tick away
29///   - Improving would not violate the limit price
30///
31/// - This algorithm will always have at most one order at a time out.
32///
33/// # Quote Positioning Strategy
34///
35/// The algorithm uses a sophisticated positioning strategy:
36/// - **Join Mode**: Places orders at the current best bid/ask on the same side
37///     - IMPORTANT: The algo joins at the BBO which INCLUDES its own order once placed
38///     - Once at the BBO, the order maintains that price level even if other orders cancel
39///     - This means the algo won't automatically back off to less aggressive prices
40///     - This behavior is a side effect of only using L1 data to determine the BBO
41///     - However, it may result in the algo being alone at a price level if others cancel
42///     - The order will only move to MORE aggressive prices (never less aggressive in Join mode)
43/// - **Improve Mode**: Places orders one tick better than the current best bid/ask if there's room
44///     - Attempts to gain queue priority by improving the market by exactly one tick
45///     - Will not cross the spread (checks opposite side before improving)
46///
47/// - This algorithm will behave differently in PAPER_TRADING than on the real exchange, because
48///   PAPER_TRADING does not affect the order book
49///
50/// The algorithm continuously monitors the market and repositions the quote as needed to maintain
51/// competitiveness while respecting the specified constraints.
52///
53/// # Use Cases
54/// - **Market Making**: Providing liquidity on one side of the market
55/// - **Spread Trading**: Working the passive leg of a spread trade
56/// - **Passive Execution**: Getting filled at favorable prices without crossing the spread
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58pub struct QuoteOneSide;
59
60impl Algo for QuoteOneSide {
61    const NAME: &'static str = "QUOTE_ONE_SIDE";
62
63    type Params = QuoteOneSideParams;
64    type Status = QuoteOneSideStatus;
65}
66
67/// Whether to improve the market or join at the current best price
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub enum ImproveOrJoin {
70    /// Improve the market by one tick when possible
71    Improve,
72    /// Join at the current best price
73    Join,
74}
75
76/// Parameters for the QuoteOneSide algorithm
77#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
78pub struct QuoteOneSideParams {
79    pub symbol: String,
80    pub marketdata_venue: MarketdataVenue,
81    pub execution_venue: ExecutionVenue,
82    pub account: Option<AccountIdOrName>,
83    pub dir: Dir,
84    pub quantity: Decimal,
85    /// the most aggressive price the algo will quote at
86    pub limit_price: Decimal,
87    /// Maximum number of ticks less aggressive than the BBO to quote
88    /// - `None`: No constraint on distance from BBO - will quote at any valid price up to the limit price
89    /// - `Some(n)`: Will only quote if within n ticks of the best same-side price (BBO)
90    ///   Orders beyond this distance are cancelled as they're unlikely to fill
91    /// - Example: With `Some(5)` for a buy order, if best bid is 100, will only quote between 95-100
92    pub max_ticks_outside: Option<Decimal>,
93    /// Whether to improve the market or join at the current best price
94    pub improve_or_join: ImproveOrJoin,
95    /// Insert as 0, used for tracking fill quantity when modifying quote
96    pub quantity_filled: Decimal,
97}
98
99impl DisplaySymbols for QuoteOneSideParams {
100    fn display_symbols(&self) -> Option<Vec<String>> {
101        Some(vec![self.symbol.clone()])
102    }
103}
104
105impl Validate for QuoteOneSideParams {
106    fn validate(&self) -> Result<()> {
107        if !self.quantity.is_sign_positive() {
108            bail!("quantity must be positive");
109        }
110        if let Some(max_ticks) = self.max_ticks_outside {
111            if !max_ticks.is_sign_positive() && max_ticks != Decimal::ZERO {
112                bail!("max_ticks_outside must be non-negative");
113            }
114            if !max_ticks.is_integer() {
115                bail!("max_ticks_outside must be an integer");
116            }
117        }
118        Ok(())
119    }
120}
121
122/// Current status of the QuoteOneSide algorithm
123#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
124pub struct QuoteOneSideStatus {
125    pub realized_avg_price: Option<Decimal>,
126    pub quantity_filled: Decimal,
127    pub current_quote_price: Option<Decimal>,
128    /// Indicates whether the current quote is at the front of the queue (best price on our side)
129    /// - For Buy orders: `true` when our quote price > previous best bid on the market
130    /// - For Sell orders: `true` when our quote price < previous best ask on the market
131    /// - Also `true` when we're the first to establish a quote on our side (no existing bid/ask)
132    /// - This status updates dynamically as market conditions change and other orders arrive/cancel
133    /// - Being front of queue provides priority for fills
134    pub front_of_queue: bool,
135    pub orders_sent: u32,
136    /// Indicates whether the algorithm is currently cancelling an order
137    pub is_cancelling: bool,
138}