use super::days_of_week::DaysOfWeek;
use super::funding_rate_schedule::FundingRateSchedule;
use crate::OrderId;
use anyhow::{anyhow, bail, Error, Result};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use strum::VariantArray;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstrumentV0 {
pub symbol: String,
pub tick_size: Decimal,
pub base_currency: String,
pub multiplier: i32,
pub minimum_trade_quantity: u64,
pub description: String,
pub product_id: String,
pub state: String,
pub price_scale: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Instrument {
pub symbol: String,
pub multiplier: Decimal,
pub price_scale: i64,
pub minimum_order_size: Decimal,
pub tick_size: Decimal,
pub quote_currency: String,
pub price_band_lower_deviation_pct: Option<Decimal>,
pub price_band_upper_deviation_pct: Option<Decimal>,
pub funding_settlement_currency: String,
pub funding_rate_cap_upper_pct: Option<Decimal>,
pub funding_rate_cap_lower_pct: Option<Decimal>,
pub maintenance_margin_pct: Decimal,
pub initial_margin_pct: Decimal,
pub category: InstrumentCategory,
pub description: Option<String>,
pub underlying_benchmark_price: Option<String>,
pub contract_mark_price: Option<String>,
pub contract_size: Option<String>,
pub price_quotation: Option<String>,
pub price_bands: Option<String>,
pub funding_schedule_time_description: Option<String>,
pub funding_schedule_calendar_description: Option<String>,
pub funding_schedule: Option<FundingRateSchedule>,
pub trading_schedule: Option<TradingSchedule>,
}
#[derive(
Copy, Clone, Debug, Eq, PartialEq, strum::Display, strum::EnumString, Serialize, Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum InstrumentCategory {
Fx,
Equities,
Metals,
EnergyEtfs,
Compute,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct TradingSchedule {
pub segments: Vec<TradingHoursSegment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct TradingHoursSegment {
pub days_of_week: DaysOfWeek,
pub time_of_day: TimeOfDay,
pub duration_seconds: u64,
pub state: InstrumentState,
pub hide_market_data: bool,
pub expire_all_orders: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct TimeOfDay {
pub hours: u8,
pub minutes: u8,
#[serde(default)]
pub seconds: u8,
}
impl TimeOfDay {
pub fn validate(&self) -> Result<()> {
if self.hours > 23 || self.minutes > 59 || self.seconds > 59 {
bail!(
"invalid time_of_day: {:02}:{:02}:{:02}",
self.hours,
self.minutes,
self.seconds
);
}
Ok(())
}
}
#[derive(Default, Debug, strum::Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum InstrumentState {
ClosedFrozen,
PreOpen,
Open,
Closed,
Delisted,
Halted,
MatchAndCloseAuction,
#[default]
Unknown,
}
#[derive(Debug, Clone)]
pub struct PlaceOrder {
pub symbol: String,
pub side: Side,
pub quantity: u64,
pub price: Decimal,
pub time_in_force: String,
pub post_only: bool,
pub tag: Option<String>,
pub clord_id: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Order {
pub order_id: OrderId,
pub user_id: String,
pub symbol: String,
pub side: Side,
pub quantity: u64,
pub price: Decimal,
pub time_in_force: String,
pub tag: Option<String>,
pub clord_id: Option<u64>,
#[serde(default)]
pub post_only: bool,
pub timestamp: DateTime<Utc>,
pub order_state: OrderState,
pub filled_quantity: u64,
pub remaining_quantity: u64,
pub completion_time: Option<DateTime<Utc>>,
pub reject_reason: Option<OrderRejectReason>,
pub reject_message: Option<String>,
}
impl Order {
pub fn is_liquidation(&self) -> bool {
self.order_id.is_liquidation()
}
}
#[derive(Debug, derive_more::Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum Side {
#[serde(rename = "B")]
Buy,
#[serde(rename = "S")]
Sell,
}
impl Side {
pub fn as_char(&self) -> &'static str {
match self {
Self::Buy => "B",
Self::Sell => "S",
}
}
pub fn from_char(s: &str) -> Result<Self> {
let t = match s {
"B" => Self::Buy,
"S" => Self::Sell,
other => bail!("unknown side: {other}"),
};
Ok(t)
}
pub fn position_sign(&self) -> i8 {
match self {
Self::Buy => 1,
Self::Sell => -1,
}
}
pub fn flip(&self) -> Self {
match self {
Self::Buy => Self::Sell,
Self::Sell => Self::Buy,
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Deserialize,
strum::EnumString,
strum::Display,
strum::IntoStaticStr,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum OrderState {
#[strum(serialize = "PENDING")]
#[serde(rename = "PENDING")]
Pending,
#[strum(serialize = "ACCEPTED")]
#[serde(rename = "ACCEPTED")]
Accepted,
#[strum(serialize = "PARTIALLY_FILLED")]
#[serde(rename = "PARTIALLY_FILLED")]
PartiallyFilled,
#[strum(serialize = "FILLED")]
#[serde(rename = "FILLED")]
Filled,
#[strum(serialize = "CANCELED")]
#[serde(rename = "CANCELED")]
Canceled,
#[strum(serialize = "REJECTED")]
#[serde(rename = "REJECTED")]
Rejected,
#[strum(serialize = "EXPIRED")]
#[serde(rename = "EXPIRED")]
Expired,
#[strum(serialize = "REPLACED")]
#[serde(rename = "REPLACED")]
Replaced,
#[strum(serialize = "DONE_FOR_DAY")]
#[serde(rename = "DONE_FOR_DAY")]
DoneForDay,
#[strum(serialize = "UNKNOWN")]
#[serde(rename = "UNKNOWN", other)]
Unknown,
}
impl OrderState {
pub fn as_str(&self) -> &'static str {
self.into()
}
pub fn is_open(&self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match self {
Self::Accepted | Self::PartiallyFilled => true,
_ => false,
}
}
pub fn is_terminal(&self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match self {
Self::Canceled
| Self::Filled
| Self::Rejected
| Self::Replaced
| Self::DoneForDay
| Self::Expired => true,
_ => false,
}
}
pub fn can_transition_to(&self, next_state: &Self) -> bool {
match self {
Self::Pending => matches!(
next_state,
Self::Pending
| Self::Accepted
| Self::Rejected
| Self::Canceled
| Self::Expired
| Self::Replaced
| Self::DoneForDay
),
Self::Accepted => matches!(
next_state,
Self::Accepted
| Self::PartiallyFilled
| Self::Filled
| Self::Canceled
| Self::Expired
| Self::Replaced
| Self::DoneForDay
),
Self::PartiallyFilled => matches!(
next_state,
Self::PartiallyFilled
| Self::Filled
| Self::Canceled
| Self::Expired
| Self::Replaced
| Self::DoneForDay
),
_ => false, }
}
pub fn can_be_canceled(&self) -> bool {
matches!(self, Self::Pending | Self::Accepted | Self::PartiallyFilled)
}
pub fn can_be_replaced(&self) -> bool {
matches!(self, Self::Accepted | Self::PartiallyFilled)
}
pub fn as_char(&self) -> &'static str {
match self {
Self::Pending => "P",
Self::Accepted => "A",
Self::PartiallyFilled => "D",
Self::Filled => "F",
Self::Canceled => "X",
Self::Rejected => "R",
Self::Expired => "E",
Self::Replaced => "K",
Self::DoneForDay => "Z",
Self::Unknown => "?",
}
}
pub fn from_char(s: &str) -> Result<Self> {
let t = match s {
"P" => Self::Pending,
"A" => Self::Accepted,
"D" => Self::PartiallyFilled,
"F" => Self::Filled,
"X" => Self::Canceled,
"R" => Self::Rejected,
"E" => Self::Expired,
"K" => Self::Replaced,
"Z" => Self::DoneForDay,
"?" => Self::Unknown,
other => bail!("unknown order state: {other}"),
};
Ok(t)
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Deserialize,
strum::EnumString,
strum::Display,
strum::IntoStaticStr,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderRejectReason {
CloseOnly,
InsufficientMargin,
MaxOpenOrdersExceeded,
UnknownSymbol,
ExchangeClosed,
IncorrectQuantity,
InvalidPriceIncrement,
IncorrectOrderType,
PriceOutOfBounds,
NoLiquidity,
InsufficientCreditLimit,
OriginalOrderTerminated,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Balance {
pub currency: String,
pub available: Decimal,
pub total: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Position {
pub symbol: String,
pub signed_quantity: i64,
pub average_price: Decimal,
pub unrealized_pnl: Decimal,
pub realized_pnl: Decimal,
pub mark_price: Decimal,
pub timestamp: DateTime<Utc>,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Candle {
pub symbol: String,
#[serde(rename = "ts")]
#[serde_as(as = "serde_with::TimestampSeconds")]
pub timestamp: DateTime<Utc>,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub buy_volume: u64,
pub sell_volume: u64,
pub volume: u64,
pub width: CandleWidth,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct BboCandle {
pub symbol: String,
#[serde(rename = "ts")]
#[serde_as(as = "serde_with::TimestampSeconds")]
pub timestamp: DateTime<Utc>,
pub bid_open: Option<Decimal>,
pub bid_high: Option<Decimal>,
pub bid_low: Option<Decimal>,
pub bid_close: Option<Decimal>,
pub ask_open: Option<Decimal>,
pub ask_high: Option<Decimal>,
pub ask_low: Option<Decimal>,
pub ask_close: Option<Decimal>,
pub mid_open: Option<Decimal>,
pub mid_high: Option<Decimal>,
pub mid_low: Option<Decimal>,
pub mid_close: Option<Decimal>,
pub width: CandleWidth,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenInterest {
pub symbol: String,
pub data: Vec<OpenInterestData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenInterestData {
pub timestamp: DateTime<Utc>,
pub open_interest: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingHistory {
pub symbol: String,
pub funding_amount: Decimal,
pub net_position: i32,
pub timestamp: DateTime<Utc>,
pub funding_rate: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepositRecord {
pub id: String,
pub symbol: String,
pub timestamp: DateTime<Utc>,
pub amount: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawalRecord {
pub id: String,
pub symbol: String,
pub timestamp: DateTime<Utc>,
pub amount: Decimal,
}
#[derive(
Copy,
Clone,
VariantArray,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
Hash,
derive_more::Display,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum CandleWidth {
#[serde(rename = "1s")]
#[display("1s")]
OneSecond,
#[serde(rename = "5s")]
#[display("5s")]
FiveSecond,
#[serde(rename = "1m")]
#[display("1m")]
OneMinute,
#[serde(rename = "5m")]
#[display("5m")]
FiveMinute,
#[serde(rename = "15m")]
#[display("15m")]
FifteenMinute,
#[serde(rename = "1h")]
#[display("1h")]
OneHour,
#[serde(rename = "1d")]
#[display("1d")]
OneDay,
}
impl std::str::FromStr for CandleWidth {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"1s" => Ok(Self::OneSecond),
"5s" => Ok(Self::FiveSecond),
"1m" => Ok(Self::OneMinute),
"5m" => Ok(Self::FiveMinute),
"15m" => Ok(Self::FifteenMinute),
"1h" => Ok(Self::OneHour),
"1d" => Ok(Self::OneDay),
_ => Err(anyhow!("unrecognized candle width: '{s}'")),
}
}
}
impl CandleWidth {
pub fn to_nanosec_window(&self, instant: u64) -> (u64, u64) {
let ns_in_sec = 1_000_000_000;
let nanosec = match self {
CandleWidth::OneSecond => ns_in_sec,
CandleWidth::FiveSecond => ns_in_sec * 5,
CandleWidth::OneMinute => ns_in_sec * 60,
CandleWidth::FiveMinute => ns_in_sec * 60 * 5,
CandleWidth::FifteenMinute => ns_in_sec * 60 * 15,
CandleWidth::OneHour => ns_in_sec * 60 * 60,
CandleWidth::OneDay => ns_in_sec * 60 * 60 * 24,
};
let start = instant - (instant % nanosec);
let end = start + nanosec - 1;
(start, end)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TIME_1: u64 = 1758050379100000000;
const TIME_2: u64 = 1758040183500000000;
const MIDNIGHT_UTC: u64 = 1757980800000000000;
const END_OF_DAY: u64 = 1758067199999999999;
const NOON: u64 = 1758024000000000000;
const JUST_BEFORE_NOON: u64 = 1758023999999999999;
const HALF_HOUR: u64 = 1758036600000000000;
const FIVE_SEC_BOUNDARY: u64 = 1758036605000000000;
const JUST_BEFORE_FIVE_SEC: u64 = 1758036604999999999;
#[test]
fn one_second_candle_window() {
let (start, end) = CandleWidth::OneSecond.to_nanosec_window(TIME_1);
assert_eq!(start, 1758050379000000000);
assert_eq!(end, 1758050379999999999);
}
#[test]
fn one_second_exact_boundary() {
let (start, end) = CandleWidth::OneSecond.to_nanosec_window(NOON);
assert_eq!(start, 1758024000000000000);
assert_eq!(end, 1758024000999999999);
}
#[test]
fn one_second_last_nanosecond() {
let (start, end) = CandleWidth::OneSecond.to_nanosec_window(JUST_BEFORE_NOON);
assert_eq!(start, 1758023999000000000);
assert_eq!(end, 1758023999999999999);
}
#[test]
fn five_second_candle_window() {
let (start, end) = CandleWidth::FiveSecond.to_nanosec_window(TIME_1);
assert_eq!(start, 1758050375000000000);
assert_eq!(end, 1758050379999999999);
}
#[test]
fn five_second_exact_boundary() {
let (start, end) = CandleWidth::FiveSecond.to_nanosec_window(FIVE_SEC_BOUNDARY);
assert_eq!(start, 1758036605000000000);
assert_eq!(end, 1758036609999999999);
}
#[test]
fn five_second_just_before_boundary() {
let (start, end) = CandleWidth::FiveSecond.to_nanosec_window(JUST_BEFORE_FIVE_SEC);
assert_eq!(start, 1758036600000000000);
assert_eq!(end, 1758036604999999999);
}
#[test]
fn five_second_at_minute_boundary() {
let (start, end) = CandleWidth::FiveSecond.to_nanosec_window(HALF_HOUR);
assert_eq!(start, 1758036600000000000);
assert_eq!(end, 1758036604999999999);
}
#[test]
fn one_minute_candle_window() {
let (start, end) = CandleWidth::OneMinute.to_nanosec_window(TIME_1);
assert_eq!(start, 1758050340000000000);
assert_eq!(end, 1758050399999999999);
}
#[test]
fn one_minute_exact_boundary() {
let (start, end) = CandleWidth::OneMinute.to_nanosec_window(HALF_HOUR);
assert_eq!(start, 1758036600000000000);
assert_eq!(end, 1758036659999999999);
}
#[test]
fn one_minute_last_nanosecond() {
let (start, end) = CandleWidth::OneMinute.to_nanosec_window(JUST_BEFORE_NOON);
assert_eq!(start, 1758023940000000000);
assert_eq!(end, 1758023999999999999);
}
#[test]
fn fifteen_minute_candle_window() {
let (start, end) = CandleWidth::FifteenMinute.to_nanosec_window(TIME_2);
assert_eq!(start, 1758039300000000000);
assert_eq!(end, 1758040199999999999);
}
#[test]
fn fifteen_minute_at_half_hour() {
let (start, end) = CandleWidth::FifteenMinute.to_nanosec_window(HALF_HOUR);
assert_eq!(start, 1758036600000000000);
assert_eq!(end, 1758037499999999999);
}
#[test]
fn fifteen_minute_at_noon() {
let (start, end) = CandleWidth::FifteenMinute.to_nanosec_window(NOON);
assert_eq!(start, 1758024000000000000);
assert_eq!(end, 1758024899999999999);
}
#[test]
fn fifteen_minute_just_before_noon() {
let (start, end) = CandleWidth::FifteenMinute.to_nanosec_window(JUST_BEFORE_NOON);
assert_eq!(start, 1758023100000000000);
assert_eq!(end, 1758023999999999999);
}
#[test]
fn one_hour_candle_window() {
let (start, end) = CandleWidth::OneHour.to_nanosec_window(TIME_2);
assert_eq!(start, 1758038400000000000);
assert_eq!(end, 1758041999999999999);
}
#[test]
fn one_hour_exact_boundary() {
let (start, end) = CandleWidth::OneHour.to_nanosec_window(NOON);
assert_eq!(start, 1758024000000000000);
assert_eq!(end, 1758027599999999999);
}
#[test]
fn one_hour_last_nanosecond_before() {
let (start, end) = CandleWidth::OneHour.to_nanosec_window(JUST_BEFORE_NOON);
assert_eq!(start, 1758020400000000000);
assert_eq!(end, 1758023999999999999);
}
#[test]
fn one_hour_at_midnight() {
let (start, end) = CandleWidth::OneHour.to_nanosec_window(MIDNIGHT_UTC);
assert_eq!(start, 1757980800000000000);
assert_eq!(end, 1757984399999999999);
}
#[test]
fn one_day_candle_window() {
let (start, end) = CandleWidth::OneDay.to_nanosec_window(TIME_1);
assert_eq!(start, 1757980800000000000);
assert_eq!(end, 1758067199999999999);
}
#[test]
fn one_day_at_midnight() {
let (start, end) = CandleWidth::OneDay.to_nanosec_window(MIDNIGHT_UTC);
assert_eq!(start, 1757980800000000000);
assert_eq!(end, 1758067199999999999);
}
#[test]
fn one_day_end_of_day() {
let (start, end) = CandleWidth::OneDay.to_nanosec_window(END_OF_DAY);
assert_eq!(start, 1757980800000000000);
assert_eq!(end, 1758067199999999999);
}
#[test]
fn one_day_at_noon() {
let (start, end) = CandleWidth::OneDay.to_nanosec_window(NOON);
assert_eq!(start, 1757980800000000000);
assert_eq!(end, 1758067199999999999);
}
#[test]
fn boundaries_are_inclusive_and_continuous() {
let time = NOON;
let (_, end1) = CandleWidth::OneSecond.to_nanosec_window(time);
let (start2, _) = CandleWidth::OneSecond.to_nanosec_window(end1 + 1);
assert_eq!(end1 + 1, start2);
let (_, end1) = CandleWidth::OneMinute.to_nanosec_window(time);
let (start2, _) = CandleWidth::OneMinute.to_nanosec_window(end1 + 1);
assert_eq!(end1 + 1, start2);
}
#[test]
fn window_widths_are_correct() {
let time = NOON;
let (start, end) = CandleWidth::OneSecond.to_nanosec_window(time);
assert_eq!(end - start + 1, 1_000_000_000);
let (start, end) = CandleWidth::FiveSecond.to_nanosec_window(time);
assert_eq!(end - start + 1, 5_000_000_000);
let (start, end) = CandleWidth::OneMinute.to_nanosec_window(time);
assert_eq!(end - start + 1, 60_000_000_000);
let (start, end) = CandleWidth::FifteenMinute.to_nanosec_window(time);
assert_eq!(end - start + 1, 900_000_000_000);
let (start, end) = CandleWidth::OneHour.to_nanosec_window(time);
assert_eq!(end - start + 1, 3_600_000_000_000);
let (start, end) = CandleWidth::OneDay.to_nanosec_window(time);
assert_eq!(end - start + 1, 86_400_000_000_000);
}
#[test]
fn test_trading_schedule_serde_roundtrip() {
let schedule = TradingSchedule {
segments: vec![
TradingHoursSegment {
days_of_week: DaysOfWeek::weekdays(),
time_of_day: TimeOfDay {
hours: 9,
minutes: 30,
seconds: 0,
},
duration_seconds: 3600,
state: InstrumentState::PreOpen,
hide_market_data: false,
expire_all_orders: false,
},
TradingHoursSegment {
days_of_week: DaysOfWeek::weekdays(),
time_of_day: TimeOfDay {
hours: 10,
minutes: 30,
seconds: 0,
},
duration_seconds: 21600,
state: InstrumentState::Open,
hide_market_data: false,
expire_all_orders: false,
},
],
};
insta::assert_json_snapshot!(schedule, @r#"
{
"segments": [
{
"days_of_week": [
1,
2,
3,
4,
5
],
"time_of_day": {
"hours": 9,
"minutes": 30,
"seconds": 0
},
"duration_seconds": 3600,
"state": "PRE_OPEN",
"hide_market_data": false,
"expire_all_orders": false
},
{
"days_of_week": [
1,
2,
3,
4,
5
],
"time_of_day": {
"hours": 10,
"minutes": 30,
"seconds": 0
},
"duration_seconds": 21600,
"state": "OPEN",
"hide_market_data": false,
"expire_all_orders": false
}
]
}
"#);
let json = serde_json::to_string(&schedule).unwrap();
let deserialized: TradingSchedule = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.segments.len(), 2);
assert_eq!(
deserialized.segments[0].days_of_week,
DaysOfWeek::weekdays()
);
assert_eq!(deserialized.segments[0].state, InstrumentState::PreOpen);
assert_eq!(deserialized.segments[1].state, InstrumentState::Open);
}
#[test]
fn test_trading_schedule_deserialization() {
let json = r#"{
"segments": [
{
"days_of_week": [1, 2, 3, 4, 5],
"time_of_day": {"hours": 9, "minutes": 30, "seconds": 0},
"duration_seconds": 1800,
"state": "PRE_OPEN",
"hide_market_data": false,
"expire_all_orders": false
},
{
"days_of_week": [1, 2, 3, 4, 5],
"time_of_day": {"hours": 10, "minutes": 0, "seconds": 0},
"duration_seconds": 21600,
"state": "OPEN",
"hide_market_data": false,
"expire_all_orders": false
}
]
}"#;
let schedule: TradingSchedule = serde_json::from_str(json).unwrap();
assert_eq!(schedule.segments.len(), 2);
let preopen = &schedule.segments[0];
assert_eq!(preopen.days_of_week, DaysOfWeek::weekdays());
assert_eq!(preopen.time_of_day.hours, 9);
assert_eq!(preopen.time_of_day.minutes, 30);
assert_eq!(preopen.duration_seconds, 1800);
assert_eq!(preopen.state, InstrumentState::PreOpen);
let open = &schedule.segments[1];
assert_eq!(open.time_of_day.hours, 10);
assert_eq!(open.time_of_day.minutes, 0);
assert_eq!(open.duration_seconds, 21600);
assert_eq!(open.state, InstrumentState::Open);
}
#[test]
fn test_instrument_state_serialization() {
assert_eq!(
serde_json::to_string(&InstrumentState::ClosedFrozen).unwrap(),
r#""CLOSED_FROZEN""#
);
assert_eq!(
serde_json::to_string(&InstrumentState::PreOpen).unwrap(),
r#""PRE_OPEN""#
);
assert_eq!(
serde_json::to_string(&InstrumentState::Open).unwrap(),
r#""OPEN""#
);
assert_eq!(
serde_json::to_string(&InstrumentState::Closed).unwrap(),
r#""CLOSED""#
);
assert_eq!(
serde_json::to_string(&InstrumentState::Delisted).unwrap(),
r#""DELISTED""#
);
assert_eq!(
serde_json::to_string(&InstrumentState::Unknown).unwrap(),
r#""UNKNOWN""#
);
}
#[test]
fn test_instrument_with_trading_schedule_serde_roundtrip() {
let instrument = Instrument {
symbol: "TEST-PERP".to_string(),
multiplier: rust_decimal::Decimal::ONE,
price_scale: 10000,
minimum_order_size: rust_decimal::Decimal::ONE,
tick_size: rust_decimal::Decimal::new(1, 4), quote_currency: "USD".to_string(),
price_band_lower_deviation_pct: Some(rust_decimal::Decimal::new(-5, 0)),
price_band_upper_deviation_pct: Some(rust_decimal::Decimal::new(5, 0)),
funding_settlement_currency: "USD".to_string(),
funding_rate_cap_upper_pct: Some(rust_decimal::Decimal::new(1, 0)),
funding_rate_cap_lower_pct: Some(rust_decimal::Decimal::new(-1, 0)),
maintenance_margin_pct: rust_decimal::Decimal::new(4, 0),
initial_margin_pct: rust_decimal::Decimal::new(8, 0),
category: InstrumentCategory::Fx,
description: Some("Test Perpetual Future".to_string()),
underlying_benchmark_price: None,
contract_mark_price: None,
contract_size: None,
price_quotation: None,
price_bands: None,
funding_schedule_time_description: None,
funding_schedule_calendar_description: None,
funding_schedule: None,
trading_schedule: Some(TradingSchedule {
segments: vec![TradingHoursSegment {
days_of_week: DaysOfWeek::weekdays(),
time_of_day: TimeOfDay {
hours: 9,
minutes: 30,
seconds: 0,
},
duration_seconds: 1800,
state: InstrumentState::PreOpen,
hide_market_data: false,
expire_all_orders: false,
}],
}),
};
insta::assert_json_snapshot!(instrument, @r#"
{
"symbol": "TEST-PERP",
"multiplier": "1",
"price_scale": 10000,
"minimum_order_size": "1",
"tick_size": "0.0001",
"quote_currency": "USD",
"price_band_lower_deviation_pct": "-5",
"price_band_upper_deviation_pct": "5",
"funding_settlement_currency": "USD",
"funding_rate_cap_upper_pct": "1",
"funding_rate_cap_lower_pct": "-1",
"maintenance_margin_pct": "4",
"initial_margin_pct": "8",
"category": "fx",
"description": "Test Perpetual Future",
"underlying_benchmark_price": null,
"contract_mark_price": null,
"contract_size": null,
"price_quotation": null,
"price_bands": null,
"funding_schedule_time_description": null,
"funding_schedule_calendar_description": null,
"funding_schedule": null,
"trading_schedule": {
"segments": [
{
"days_of_week": [
1,
2,
3,
4,
5
],
"time_of_day": {
"hours": 9,
"minutes": 30,
"seconds": 0
},
"duration_seconds": 1800,
"state": "PRE_OPEN",
"hide_market_data": false,
"expire_all_orders": false
}
]
}
}
"#);
let json = serde_json::to_string(&instrument).unwrap();
let deserialized: Instrument = serde_json::from_str(&json).unwrap();
assert!(deserialized.trading_schedule.is_some());
assert_eq!(deserialized.trading_schedule.unwrap().segments.len(), 1);
}
}