use anyhow::{bail, Result};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use strum::VariantArray;
use uuid::Uuid;
#[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: i32,
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 state: InstrumentState,
pub multiplier: Decimal,
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 finding_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 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_frequency: Option<String>,
pub funding_calendar_schedule: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InstrumentState {
PreOpen,
Open,
Suspended,
Delisted,
#[default]
Unknown,
}
#[derive(Debug, Clone)]
pub struct PlaceOrder {
pub symbol: String,
pub side: Side,
pub quantity: i32,
pub price: Decimal,
pub time_in_force: String,
pub post_only: bool,
pub tag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Order {
pub order_id: String,
pub user_id: Uuid,
pub symbol: String,
pub side: Side,
pub quantity: i32,
pub price: Decimal,
pub time_in_force: String,
pub tag: Option<String>,
pub timestamp: DateTime<Utc>,
pub order_state: OrderState,
pub filled_quantity: i32,
pub remaining_quantity: i32,
pub completion_time: Option<DateTime<Utc>>,
pub reject_reason: Option<OrderRejectReason>,
pub reject_message: Option<String>,
}
#[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<'a>(s: &'a 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,
}
}
}
#[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")]
Unknown,
}
impl OrderState {
pub fn as_str(&self) -> &'static str {
self.into()
}
pub fn is_open(&self) -> bool {
match self {
Self::Accepted | Self::PartiallyFilled => true,
_ => false,
}
}
pub fn is_terminal(&self) -> bool {
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<'a>(s: &'a 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,
#[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 quantity: Decimal,
pub average_price: Decimal,
pub unrealized_pnl: Decimal,
pub realized_pnl: Decimal,
pub mark_price: Decimal,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fill {
pub fill_id: String,
pub order_id: String,
pub symbol: String,
pub side: Side,
pub quantity: Decimal,
pub price: Decimal,
pub fee: 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: i64,
pub sell_volume: i64,
pub volume: i64,
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 = String;
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(format!("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 * 1,
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);
}
}