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}