1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use super::*;
use crate::{
    algo::generic_container::AlgoContainerMessage, symbology::MarketId, DirPair,
    HumanDuration, OrderId, Str,
};
use anyhow::bail;
use derive::FromValue;
use netidx_derive::Pack;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};

pub type MMAlgoMessage =
    AlgoContainerMessage<MMAlgoOrder, AlgoPreview, MMAlgoStatus, AlgoLog>;

#[derive(Debug, Clone, Copy, Pack, FromValue, Serialize, Deserialize)]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
pub enum ReferencePrice {
    Mid,
    BidAsk,
    HedgeMarketBidAsk,
}

#[derive(Debug, Clone, Copy, Pack, FromValue, Serialize, Deserialize)]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
pub struct HedgeMarket {
    pub market: MarketId,
    pub conversion_ratio: Decimal,
    pub premium: Decimal,
    pub hedge_frac: Decimal,
}

#[derive(Debug, Clone, Copy, Pack, FromValue, Serialize, Deserialize)]
pub struct MMAlgoOrder {
    pub order_id: OrderId,
    pub market: MarketId,
    pub trader: UserId,
    pub account: Option<AccountId>,
    pub quantity: DirPair<Decimal>,
    pub min_position: Decimal,
    pub max_position: Decimal,
    pub max_improve_bbo: Decimal,
    pub position_tilt: Decimal,
    pub reference_price: ReferencePrice,
    pub ref_dist_frac: Decimal,
    pub tolerance_frac: Decimal,
    pub fill_lockout: HumanDuration,
    pub order_lockout: HumanDuration,
    pub reject_lockout: HumanDuration,
    pub hedge_market: Option<HedgeMarket>,
}

impl Into<AlgoOrder> for &MMAlgoOrder {
    fn into(self) -> AlgoOrder {
        let algo = if self.hedge_market.is_some() { "Spread" } else { "MM" };
        AlgoOrder {
            order_id: self.order_id,
            trader: self.trader,
            algo: Str::try_from(algo).unwrap(),
        }
    }
}

impl Validate for MMAlgoOrder {
    fn validate(&self) -> Result<()> {
        if !self.quantity.buy.is_sign_positive() {
            bail!("quantity.buy must be positive");
        }
        if !self.quantity.sell.is_sign_positive() {
            bail!("quantity.sell must be positive");
        }
        if self.min_position >= self.max_position {
            bail!("min_position must be < max_position");
        }
        if self.position_tilt.is_sign_negative() {
            bail!("position_tilt must be non-negative");
        }
        if self.ref_dist_frac < dec!(0.0001) || self.ref_dist_frac > dec!(0.25) {
            bail!("ref_dist_frac must be between 1bp and 25%");
        }
        if self.tolerance_frac < dec!(0.0001) || self.tolerance_frac > dec!(0.25) {
            bail!("tolerance_frac must be between 1bp and 25%");
        }
        if self.reject_lockout.num_milliseconds() < 500
            || self.reject_lockout.num_seconds() > 300
        {
            bail!("reject_lockout must be between 0.5 seconds and 300 seconds");
        }
        if self.order_lockout.num_milliseconds() < 500
            || self.order_lockout.num_seconds() > 300
        {
            bail!("order_lockout must be between 0.5 seconds and 300 seconds");
        }
        if self.fill_lockout.num_milliseconds() < 500
            || self.fill_lockout.num_seconds() > 300
        {
            bail!("fill_lockout must be between 0.5 seconds and 300 seconds");
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize)]
pub struct MMAlgoStatus {
    #[serde(flatten)]
    pub algo_status: AlgoStatus,
    pub position: Decimal,
    pub hedge_position: Decimal,
    pub sides: DirPair<Side>,
    pub kind: MMAlgoKind,
}

impl TryInto<AlgoStatus> for &MMAlgoStatus {
    type Error = ();

    fn try_into(self) -> Result<AlgoStatus, ()> {
        Ok(self.algo_status)
    }
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
pub enum MMAlgoKind {
    MM,
    Spread,
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize)]
pub enum Decision {
    DoNothing(Vec<Reason>),
    Cancel(OrderId, Vec<Reason>),
    Send { price: Decimal, quantity: Decimal },
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize)]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
pub enum Reason {
    AlgoPaused,
    AlgoStopped,
    MinPosition,
    MaxPosition,
    WithinFillLockout,
    WithinRejectLockout,
    WithinOrderLockout,
    NoReferencePrice,
    NoReferenceSize,
    NoBid,
    NoAsk,
    OpenOrderWithinTolerance,
    OpenOrderOutsideTolerance,
    CancelPending,
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize)]
pub struct Side {
    pub last_decision: Decision,
    pub last_order_time: DateTime<Utc>,
    pub last_fill_time: DateTime<Utc>,
    pub last_reject_time: DateTime<Utc>,
    pub open_order: Option<OpenOrder>,
    pub reference_price: Option<Decimal>,
}

impl Side {
    pub fn new() -> Self {
        Self {
            last_decision: Decision::DoNothing(vec![]),
            last_order_time: DateTime::<Utc>::MIN_UTC,
            last_fill_time: DateTime::<Utc>::MIN_UTC,
            last_reject_time: DateTime::<Utc>::MIN_UTC,
            open_order: None,
            reference_price: None,
        }
    }
}

#[derive(Debug, Clone, Pack, FromValue, Serialize, Deserialize)]
pub struct OpenOrder {
    pub order_id: OrderId,
    pub price: Decimal,
    pub quantity: Decimal,
    pub cancel_pending: bool,
}