use std::collections::BTreeMap;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use serde_repr::{Deserialize_repr, Serialize_repr};
use thiserror::Error;
use crate::{
AccountId, ContractId, OrderId, PositionId, ProviderDate, SymbolId, Timestamp, TradeId,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Side {
Bid,
Ask,
Unknown(i32),
}
impl Side {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Bid => 0,
Self::Ask => 1,
Self::Unknown(code) => code,
}
}
}
impl Serialize for Side {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for Side {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
0 => Self::Bid,
1 => Self::Ask,
code => Self::Unknown(code),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OrderType {
Limit,
Market,
StopLimit,
Stop,
TrailingStop,
JoinBid,
JoinAsk,
Unknown(i32),
}
impl OrderType {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Limit => 1,
Self::Market => 2,
Self::StopLimit => 3,
Self::Stop => 4,
Self::TrailingStop => 5,
Self::JoinBid => 6,
Self::JoinAsk => 7,
Self::Unknown(code) => code,
}
}
}
impl Serialize for OrderType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for OrderType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
1 => Self::Limit,
2 => Self::Market,
3 => Self::StopLimit,
4 => Self::Stop,
5 => Self::TrailingStop,
6 => Self::JoinBid,
7 => Self::JoinAsk,
code => Self::Unknown(code),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OrderStatus {
None,
Open,
Filled,
Cancelled,
Expired,
Rejected,
Pending,
PendingCancellation,
Suspended,
Unknown(i32),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr)]
#[non_exhaustive]
#[repr(i32)]
pub enum OrderSortBy {
CreatedAt = 0,
Id = 1,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr)]
#[non_exhaustive]
#[repr(i32)]
pub enum OrderSortDirection {
Ascending = 0,
Descending = 1,
}
impl OrderStatus {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::None => 0,
Self::Open => 1,
Self::Filled => 2,
Self::Cancelled => 3,
Self::Expired => 4,
Self::Rejected => 5,
Self::Pending => 6,
Self::PendingCancellation => 7,
Self::Suspended => 8,
Self::Unknown(code) => code,
}
}
}
impl Serialize for OrderStatus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for OrderStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
0 => Self::None,
1 => Self::Open,
2 => Self::Filled,
3 => Self::Cancelled,
4 => Self::Expired,
5 => Self::Rejected,
6 => Self::Pending,
7 => Self::PendingCancellation,
8 => Self::Suspended,
code => Self::Unknown(code),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TradeLogType {
Buy,
Sell,
Unknown(i32),
}
impl TradeLogType {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Buy => 0,
Self::Sell => 1,
Self::Unknown(code) => code,
}
}
}
impl Serialize for TradeLogType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for TradeLogType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
0 => Self::Buy,
1 => Self::Sell,
code => Self::Unknown(code),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PositionType {
Undefined,
Long,
Short,
Unknown(i32),
}
impl PositionType {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Undefined => 0,
Self::Long => 1,
Self::Short => 2,
Self::Unknown(code) => code,
}
}
}
impl Serialize for PositionType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for PositionType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
0 => Self::Undefined,
1 => Self::Long,
2 => Self::Short,
code => Self::Unknown(code),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DepthType {
Unknown,
Ask,
Bid,
BestAsk,
BestBid,
Trade,
Reset,
Low,
High,
NewBestBid,
NewBestAsk,
Fill,
UnknownCode(i32),
}
impl DepthType {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Unknown => 0,
Self::Ask => 1,
Self::Bid => 2,
Self::BestAsk => 3,
Self::BestBid => 4,
Self::Trade => 5,
Self::Reset => 6,
Self::Low => 7,
Self::High => 8,
Self::NewBestBid => 9,
Self::NewBestAsk => 10,
Self::Fill => 11,
Self::UnknownCode(code) => code,
}
}
}
impl Serialize for DepthType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.code())
}
}
impl<'de> Deserialize<'de> for DepthType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match i32::deserialize(deserializer)? {
0 => Self::Unknown,
1 => Self::Ask,
2 => Self::Bid,
3 => Self::BestAsk,
4 => Self::BestBid,
5 => Self::Trade,
6 => Self::Reset,
7 => Self::Low,
8 => Self::High,
9 => Self::NewBestBid,
10 => Self::NewBestAsk,
11 => Self::Fill,
code => Self::UnknownCode(code),
})
}
}
#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, PartialEq, Serialize_repr)]
#[non_exhaustive]
#[repr(i32)]
pub enum BarUnit {
Second = 1,
Minute = 2,
Hour = 3,
Day = 4,
Week = 5,
Month = 6,
Tick = 7,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub id: AccountId,
pub name: String,
#[serde(default, with = "crate::decimal_serde::option")]
pub balance: Option<Decimal>,
pub can_trade: bool,
pub is_visible: bool,
#[serde(default)]
pub simulated: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Contract {
pub id: ContractId,
pub name: String,
pub description: String,
#[serde(with = "crate::decimal_serde")]
pub tick_size: Decimal,
#[serde(with = "crate::decimal_serde")]
pub tick_value: Decimal,
pub active_contract: bool,
pub symbol_id: SymbolId,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchContracts {
pub live: bool,
pub search_text: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoryRequest {
contract_id: ContractId,
live: bool,
start_time: Timestamp,
end_time: Timestamp,
unit: BarUnit,
unit_number: i32,
limit: i32,
include_partial_bar: bool,
}
impl HistoryRequest {
pub fn builder(
contract_id: ContractId,
live: bool,
start_time: Timestamp,
end_time: Timestamp,
unit: BarUnit,
) -> HistoryRequestBuilder {
HistoryRequestBuilder {
contract_id,
live,
start_time,
end_time,
unit,
unit_number: 1,
limit: 20_000,
include_partial_bar: false,
}
}
#[must_use]
pub const fn contract_id(&self) -> &ContractId {
&self.contract_id
}
#[must_use]
pub const fn is_live(&self) -> bool {
self.live
}
#[must_use]
pub const fn start_time(&self) -> Timestamp {
self.start_time
}
#[must_use]
pub const fn end_time(&self) -> Timestamp {
self.end_time
}
#[must_use]
pub const fn unit(&self) -> BarUnit {
self.unit
}
#[must_use]
pub const fn unit_number(&self) -> i32 {
self.unit_number
}
#[must_use]
pub const fn limit(&self) -> i32 {
self.limit
}
#[must_use]
pub const fn includes_partial_bar(&self) -> bool {
self.include_partial_bar
}
}
#[derive(Clone, Debug)]
#[must_use = "a HistoryRequestBuilder does nothing until build is called"]
pub struct HistoryRequestBuilder {
contract_id: ContractId,
live: bool,
start_time: Timestamp,
end_time: Timestamp,
unit: BarUnit,
unit_number: i32,
limit: i32,
include_partial_bar: bool,
}
impl HistoryRequestBuilder {
pub const fn unit_number(mut self, unit_number: i32) -> Self {
self.unit_number = unit_number;
self
}
pub const fn limit(mut self, limit: i32) -> Self {
self.limit = limit;
self
}
pub const fn include_partial_bar(mut self, include: bool) -> Self {
self.include_partial_bar = include;
self
}
pub fn build(self) -> Result<HistoryRequest, RequestValidationError> {
if self.start_time >= self.end_time {
return Err(RequestValidationError::HistoryRangeNotIncreasing);
}
if self.unit_number <= 0 {
return Err(RequestValidationError::NonPositiveHistoryUnitNumber);
}
if !(1..=20_000).contains(&self.limit) {
return Err(RequestValidationError::HistoryLimitOutOfRange);
}
Ok(HistoryRequest {
contract_id: self.contract_id,
live: self.live,
start_time: self.start_time,
end_time: self.end_time,
unit: self.unit,
unit_number: self.unit_number,
limit: self.limit,
include_partial_bar: self.include_partial_bar,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct Bar {
pub t: Timestamp,
#[serde(with = "crate::decimal_serde")]
pub o: Decimal,
#[serde(with = "crate::decimal_serde")]
pub h: Decimal,
#[serde(with = "crate::decimal_serde")]
pub l: Decimal,
#[serde(with = "crate::decimal_serde")]
pub c: Decimal,
pub v: i64,
#[serde(default)]
pub d: Option<ProviderDate>,
#[serde(default)]
pub k: Option<i64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderSearch {
account_id: AccountId,
start_timestamp: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
end_timestamp: Option<Timestamp>,
}
impl OrderSearch {
pub fn new(
account_id: AccountId,
start_timestamp: Timestamp,
end_timestamp: Option<Timestamp>,
) -> Result<Self, RequestValidationError> {
validate_search_range(Some(start_timestamp), end_timestamp)?;
Ok(Self {
account_id,
start_timestamp,
end_timestamp,
})
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn start_timestamp(&self) -> Timestamp {
self.start_timestamp
}
#[must_use]
pub const fn end_timestamp(&self) -> Option<Timestamp> {
self.end_timestamp
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderQuery {
filter: OrderFilter,
#[serde(skip_serializing_if = "Option::is_none")]
page_size: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
page_offset: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
sort_by: Option<OrderSortBy>,
#[serde(skip_serializing_if = "Option::is_none")]
sort_direction: Option<OrderSortDirection>,
#[serde(skip_serializing_if = "Option::is_none")]
include_total_count: Option<bool>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct OrderFilter {
account_id: AccountId,
#[serde(skip_serializing_if = "Vec::is_empty")]
statuses: Vec<OrderStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
contract_id: Option<ContractId>,
#[serde(skip_serializing_if = "Option::is_none")]
created_after: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
created_before: Option<Timestamp>,
}
impl OrderQuery {
pub fn builder(account_id: AccountId) -> OrderQueryBuilder {
OrderQueryBuilder {
account_id,
statuses: Vec::new(),
contract_id: None,
created_after: None,
created_before: None,
page_size: None,
page_offset: None,
sort_by: None,
sort_direction: None,
include_total_count: None,
}
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.filter.account_id
}
#[must_use]
pub fn statuses(&self) -> &[OrderStatus] {
&self.filter.statuses
}
#[must_use]
pub const fn contract_id(&self) -> Option<&ContractId> {
self.filter.contract_id.as_ref()
}
#[must_use]
pub const fn created_after(&self) -> Option<Timestamp> {
self.filter.created_after
}
#[must_use]
pub const fn created_before(&self) -> Option<Timestamp> {
self.filter.created_before
}
#[must_use]
pub const fn page_size(&self) -> Option<i32> {
self.page_size
}
#[must_use]
pub const fn page_offset(&self) -> Option<i32> {
self.page_offset
}
#[must_use]
pub const fn sort_by(&self) -> Option<OrderSortBy> {
self.sort_by
}
#[must_use]
pub const fn sort_direction(&self) -> Option<OrderSortDirection> {
self.sort_direction
}
#[must_use]
pub const fn include_total_count(&self) -> Option<bool> {
self.include_total_count
}
}
#[derive(Clone, Debug)]
#[must_use = "an OrderQueryBuilder does nothing until build is called"]
pub struct OrderQueryBuilder {
account_id: AccountId,
statuses: Vec<OrderStatus>,
contract_id: Option<ContractId>,
created_after: Option<Timestamp>,
created_before: Option<Timestamp>,
page_size: Option<i32>,
page_offset: Option<i32>,
sort_by: Option<OrderSortBy>,
sort_direction: Option<OrderSortDirection>,
include_total_count: Option<bool>,
}
impl OrderQueryBuilder {
pub fn statuses(mut self, statuses: impl IntoIterator<Item = OrderStatus>) -> Self {
self.statuses = statuses.into_iter().collect();
self
}
pub fn contract_id(mut self, contract_id: ContractId) -> Self {
self.contract_id = Some(contract_id);
self
}
pub const fn created_after(mut self, created_after: Timestamp) -> Self {
self.created_after = Some(created_after);
self
}
pub const fn created_before(mut self, created_before: Timestamp) -> Self {
self.created_before = Some(created_before);
self
}
pub const fn page_size(mut self, page_size: i32) -> Self {
self.page_size = Some(page_size);
self
}
pub const fn page_offset(mut self, page_offset: i32) -> Self {
self.page_offset = Some(page_offset);
self
}
pub const fn sort_by(mut self, sort_by: OrderSortBy) -> Self {
self.sort_by = Some(sort_by);
self
}
pub const fn sort_direction(mut self, sort_direction: OrderSortDirection) -> Self {
self.sort_direction = Some(sort_direction);
self
}
pub const fn include_total_count(mut self, include: bool) -> Self {
self.include_total_count = Some(include);
self
}
pub fn build(self) -> Result<OrderQuery, RequestValidationError> {
if let Some(code) = self.statuses.iter().find_map(|status| match status {
OrderStatus::Unknown(code) => Some(*code),
_ => None,
}) {
return Err(RequestValidationError::UnsupportedOrderStatus { code });
}
if self
.created_after
.zip(self.created_before)
.is_some_and(|(after, before)| after >= before)
{
return Err(RequestValidationError::SearchRangeNotIncreasing);
}
if self.page_size.is_some_and(|size| size <= 0) {
return Err(RequestValidationError::NonPositiveOrderPageSize);
}
if self.page_offset.is_some_and(|offset| offset < 0) {
return Err(RequestValidationError::NegativeOrderPageOffset);
}
Ok(OrderQuery {
filter: OrderFilter {
account_id: self.account_id,
statuses: self.statuses,
contract_id: self.contract_id,
created_after: self.created_after,
created_before: self.created_before,
},
page_size: self.page_size,
page_offset: self.page_offset,
sort_by: self.sort_by,
sort_direction: self.sort_direction,
include_total_count: self.include_total_count,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct OrderPage {
#[serde(default, deserialize_with = "null_to_empty")]
pub orders: Vec<Order>,
#[serde(default)]
pub total_count: Option<i32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub id: OrderId,
pub account_id: AccountId,
pub contract_id: ContractId,
#[serde(default)]
pub symbol_id: Option<SymbolId>,
pub creation_timestamp: Timestamp,
pub update_timestamp: Timestamp,
pub status: OrderStatus,
#[serde(rename = "type")]
pub order_type: OrderType,
pub side: Side,
pub size: i32,
#[serde(default, with = "crate::decimal_serde::option")]
pub limit_price: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub stop_price: Option<Decimal>,
#[serde(default)]
pub fill_volume: Option<i32>,
#[serde(default, with = "crate::decimal_serde::option")]
pub filled_price: Option<Decimal>,
#[serde(default)]
pub custom_tag: Option<String>,
#[serde(default)]
pub trail_distance: Option<i32>,
#[serde(default, with = "crate::decimal_serde::option")]
pub trail_price: Option<Decimal>,
#[serde(default)]
pub parent_order_id: Option<OrderId>,
#[serde(default)]
pub linked_order_id: Option<OrderId>,
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum RequestValidationError {
#[error("order size must be positive")]
NonPositiveOrderSize,
#[error("replacement order size must be positive")]
NonPositiveReplacementSize,
#[error("order modification requires at least one replacement value")]
EmptyModification,
#[error("bracket ticks must be positive")]
NonPositiveBracketTicks,
#[error("unsupported order type code {code}")]
UnsupportedOrderType {
code: i32,
},
#[error("unsupported order side code {code}")]
UnsupportedOrderSide {
code: i32,
},
#[error("unsupported order status code {code}")]
UnsupportedOrderStatus {
code: i32,
},
#[error("order-query page size must be positive")]
NonPositiveOrderPageSize,
#[error("order-query page offset must not be negative")]
NegativeOrderPageOffset,
#[error("historical-bar unit number must be positive")]
NonPositiveHistoryUnitNumber,
#[error("historical-bar limit must be between 1 and 20,000")]
HistoryLimitOutOfRange,
#[error("historical-bar end time must be later than its start time")]
HistoryRangeNotIncreasing,
#[error("search end time must be later than its start time")]
SearchRangeNotIncreasing,
#[error("partial-close size must be positive")]
NonPositivePartialCloseSize,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Bracket {
ticks: i32,
#[serde(rename = "type")]
order_type: OrderType,
}
impl Bracket {
pub fn new(ticks: i32, order_type: OrderType) -> Result<Self, RequestValidationError> {
if ticks <= 0 {
return Err(RequestValidationError::NonPositiveBracketTicks);
}
validate_request_order_type(order_type)?;
Ok(Self { ticks, order_type })
}
#[must_use]
pub const fn ticks(&self) -> i32 {
self.ticks
}
#[must_use]
pub const fn order_type(&self) -> OrderType {
self.order_type
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaceOrder {
account_id: AccountId,
contract_id: ContractId,
#[serde(rename = "type")]
order_type: OrderType,
side: Side,
size: i32,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
limit_price: Option<Decimal>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
stop_price: Option<Decimal>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
trail_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
custom_tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_loss_bracket: Option<Bracket>,
#[serde(skip_serializing_if = "Option::is_none")]
take_profit_bracket: Option<Bracket>,
}
impl PlaceOrder {
pub fn builder(
account_id: AccountId,
contract_id: ContractId,
order_type: OrderType,
side: Side,
quantity: i32,
) -> PlaceOrderBuilder {
PlaceOrderBuilder {
account_id,
contract_id,
order_type,
side,
size: quantity,
limit_price: None,
stop_price: None,
trail_price: None,
custom_tag: None,
stop_loss_bracket: None,
take_profit_bracket: None,
}
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn contract_id(&self) -> &ContractId {
&self.contract_id
}
#[must_use]
pub const fn order_type(&self) -> OrderType {
self.order_type
}
#[must_use]
pub const fn side(&self) -> Side {
self.side
}
#[must_use]
pub const fn size(&self) -> i32 {
self.size
}
#[must_use]
pub const fn limit_price(&self) -> Option<Decimal> {
self.limit_price
}
#[must_use]
pub const fn stop_price(&self) -> Option<Decimal> {
self.stop_price
}
#[must_use]
pub const fn trail_price(&self) -> Option<Decimal> {
self.trail_price
}
#[must_use]
pub fn custom_tag(&self) -> Option<&str> {
self.custom_tag.as_deref()
}
#[must_use]
pub const fn stop_loss_bracket(&self) -> Option<&Bracket> {
self.stop_loss_bracket.as_ref()
}
#[must_use]
pub const fn take_profit_bracket(&self) -> Option<&Bracket> {
self.take_profit_bracket.as_ref()
}
}
#[derive(Clone, Debug)]
#[must_use = "a PlaceOrderBuilder does nothing until build is called"]
pub struct PlaceOrderBuilder {
account_id: AccountId,
contract_id: ContractId,
order_type: OrderType,
side: Side,
size: i32,
limit_price: Option<Decimal>,
stop_price: Option<Decimal>,
trail_price: Option<Decimal>,
custom_tag: Option<String>,
stop_loss_bracket: Option<Bracket>,
take_profit_bracket: Option<Bracket>,
}
impl PlaceOrderBuilder {
pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
self.limit_price = Some(limit_price);
self
}
pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
self.stop_price = Some(stop_price);
self
}
pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
self.trail_price = Some(trail_price);
self
}
pub fn custom_tag(mut self, custom_tag: impl Into<String>) -> Self {
self.custom_tag = Some(custom_tag.into());
self
}
pub fn stop_loss_bracket(mut self, stop_loss_bracket: Bracket) -> Self {
self.stop_loss_bracket = Some(stop_loss_bracket);
self
}
pub fn take_profit_bracket(mut self, take_profit_bracket: Bracket) -> Self {
self.take_profit_bracket = Some(take_profit_bracket);
self
}
pub fn build(self) -> Result<PlaceOrder, RequestValidationError> {
if self.size <= 0 {
return Err(RequestValidationError::NonPositiveOrderSize);
}
validate_request_order_type(self.order_type)?;
if let Side::Unknown(code) = self.side {
return Err(RequestValidationError::UnsupportedOrderSide { code });
}
Ok(PlaceOrder {
account_id: self.account_id,
contract_id: self.contract_id,
order_type: self.order_type,
side: self.side,
size: self.size,
limit_price: self.limit_price,
stop_price: self.stop_price,
trail_price: self.trail_price,
custom_tag: self.custom_tag,
stop_loss_bracket: self.stop_loss_bracket,
take_profit_bracket: self.take_profit_bracket,
})
}
}
fn validate_request_order_type(order_type: OrderType) -> Result<(), RequestValidationError> {
match order_type {
OrderType::Limit
| OrderType::Market
| OrderType::Stop
| OrderType::TrailingStop
| OrderType::JoinBid
| OrderType::JoinAsk => Ok(()),
OrderType::StopLimit => Err(RequestValidationError::UnsupportedOrderType { code: 3 }),
OrderType::Unknown(code) => Err(RequestValidationError::UnsupportedOrderType { code }),
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct OrderResponse {
pub order_id: OrderId,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelOrder {
pub account_id: AccountId,
pub order_id: OrderId,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModifyOrder {
account_id: AccountId,
order_id: OrderId,
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<i32>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
limit_price: Option<Decimal>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
stop_price: Option<Decimal>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::decimal_serde::option"
)]
trail_price: Option<Decimal>,
}
impl ModifyOrder {
pub const fn builder(account_id: AccountId, order_id: OrderId) -> ModifyOrderBuilder {
ModifyOrderBuilder {
account_id,
order_id,
size: None,
limit_price: None,
stop_price: None,
trail_price: None,
}
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn order_id(&self) -> OrderId {
self.order_id
}
#[must_use]
pub const fn size(&self) -> Option<i32> {
self.size
}
#[must_use]
pub const fn limit_price(&self) -> Option<Decimal> {
self.limit_price
}
#[must_use]
pub const fn stop_price(&self) -> Option<Decimal> {
self.stop_price
}
#[must_use]
pub const fn trail_price(&self) -> Option<Decimal> {
self.trail_price
}
}
#[derive(Clone, Copy, Debug)]
#[must_use = "a ModifyOrderBuilder does nothing until build is called"]
pub struct ModifyOrderBuilder {
account_id: AccountId,
order_id: OrderId,
size: Option<i32>,
limit_price: Option<Decimal>,
stop_price: Option<Decimal>,
trail_price: Option<Decimal>,
}
impl ModifyOrderBuilder {
pub const fn size(mut self, size: i32) -> Self {
self.size = Some(size);
self
}
pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
self.limit_price = Some(limit_price);
self
}
pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
self.stop_price = Some(stop_price);
self
}
pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
self.trail_price = Some(trail_price);
self
}
pub fn build(self) -> Result<ModifyOrder, RequestValidationError> {
if self.size.is_some_and(|size| size <= 0) {
return Err(RequestValidationError::NonPositiveReplacementSize);
}
if self.size.is_none()
&& self.limit_price.is_none()
&& self.stop_price.is_none()
&& self.trail_price.is_none()
{
return Err(RequestValidationError::EmptyModification);
}
Ok(ModifyOrder {
account_id: self.account_id,
order_id: self.order_id,
size: self.size,
limit_price: self.limit_price,
stop_price: self.stop_price,
trail_price: self.trail_price,
})
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseContract {
pub account_id: AccountId,
pub contract_id: ContractId,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PartialCloseContract {
account_id: AccountId,
contract_id: ContractId,
size: i32,
}
impl PartialCloseContract {
pub fn new(
account_id: AccountId,
contract_id: ContractId,
size: i32,
) -> Result<Self, RequestValidationError> {
if size <= 0 {
return Err(RequestValidationError::NonPositivePartialCloseSize);
}
Ok(Self {
account_id,
contract_id,
size,
})
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn contract_id(&self) -> &ContractId {
&self.contract_id
}
#[must_use]
pub const fn size(&self) -> i32 {
self.size
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub id: PositionId,
pub account_id: AccountId,
pub contract_id: ContractId,
#[serde(default)]
pub contract_display_name: Option<String>,
pub creation_timestamp: Timestamp,
#[serde(rename = "type")]
pub position_type: PositionType,
pub size: i32,
#[serde(with = "crate::decimal_serde")]
pub average_price: Decimal,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeSearch {
account_id: AccountId,
start_timestamp: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
end_timestamp: Option<Timestamp>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeQuery {
account_id: AccountId,
#[serde(skip_serializing_if = "Option::is_none")]
start_timestamp: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
end_timestamp: Option<Timestamp>,
}
impl TradeQuery {
pub const fn builder(account_id: AccountId) -> TradeQueryBuilder {
TradeQueryBuilder {
account_id,
start_timestamp: None,
end_timestamp: None,
}
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn start_timestamp(&self) -> Option<Timestamp> {
self.start_timestamp
}
#[must_use]
pub const fn end_timestamp(&self) -> Option<Timestamp> {
self.end_timestamp
}
}
#[derive(Clone, Copy, Debug)]
#[must_use = "a TradeQueryBuilder does nothing until build is called"]
pub struct TradeQueryBuilder {
account_id: AccountId,
start_timestamp: Option<Timestamp>,
end_timestamp: Option<Timestamp>,
}
impl TradeQueryBuilder {
pub const fn start_timestamp(mut self, start_timestamp: Timestamp) -> Self {
self.start_timestamp = Some(start_timestamp);
self
}
pub const fn end_timestamp(mut self, end_timestamp: Timestamp) -> Self {
self.end_timestamp = Some(end_timestamp);
self
}
pub fn build(self) -> Result<TradeQuery, RequestValidationError> {
validate_search_range(self.start_timestamp, self.end_timestamp)?;
Ok(TradeQuery {
account_id: self.account_id,
start_timestamp: self.start_timestamp,
end_timestamp: self.end_timestamp,
})
}
}
impl TradeSearch {
pub fn new(
account_id: AccountId,
start_timestamp: Timestamp,
end_timestamp: Option<Timestamp>,
) -> Result<Self, RequestValidationError> {
validate_search_range(Some(start_timestamp), end_timestamp)?;
Ok(Self {
account_id,
start_timestamp,
end_timestamp,
})
}
#[must_use]
pub const fn account_id(&self) -> AccountId {
self.account_id
}
#[must_use]
pub const fn start_timestamp(&self) -> Timestamp {
self.start_timestamp
}
#[must_use]
pub const fn end_timestamp(&self) -> Option<Timestamp> {
self.end_timestamp
}
}
fn validate_search_range(
start_timestamp: Option<Timestamp>,
end_timestamp: Option<Timestamp>,
) -> Result<(), RequestValidationError> {
if start_timestamp
.zip(end_timestamp)
.is_some_and(|(start, end)| end <= start)
{
Err(RequestValidationError::SearchRangeNotIncreasing)
} else {
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub id: TradeId,
pub account_id: AccountId,
pub contract_id: ContractId,
pub creation_timestamp: Timestamp,
#[serde(with = "crate::decimal_serde")]
pub price: Decimal,
#[serde(default, with = "crate::decimal_serde::option")]
pub profit_and_loss: Option<Decimal>,
#[serde(with = "crate::decimal_serde")]
pub fees: Decimal,
#[serde(default, with = "crate::decimal_serde::option")]
pub commissions: Option<Decimal>,
pub side: Side,
pub size: i32,
pub voided: bool,
pub order_id: OrderId,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct MarketQuote {
#[serde(alias = "symbol")]
pub raw_symbol: SymbolId,
#[serde(default)]
pub symbol_name: Option<String>,
#[serde(default, with = "crate::decimal_serde::option")]
pub last_price: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub best_bid: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub best_ask: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub change: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub change_percent: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub open: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub high: Option<Decimal>,
#[serde(default, with = "crate::decimal_serde::option")]
pub low: Option<Decimal>,
#[serde(default)]
pub volume: Option<i64>,
pub last_updated: Timestamp,
#[serde(default)]
pub timestamp: Option<Timestamp>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct MarketDepth {
#[serde(default, alias = "symbolId")]
pub symbol_id: Option<SymbolId>,
pub timestamp: Timestamp,
#[serde(rename = "type")]
pub depth_type: DepthType,
#[serde(with = "crate::decimal_serde")]
pub price: Decimal,
pub volume: i64,
pub current_volume: i64,
#[serde(default)]
pub index: Option<i32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct MarketTrade {
pub symbol_id: SymbolId,
#[serde(with = "crate::decimal_serde")]
pub price: Decimal,
pub timestamp: Timestamp,
#[serde(rename = "type")]
pub trade_type: TradeLogType,
pub volume: i64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct OperationResponse;
#[derive(Debug)]
pub(crate) enum Envelope<T> {
Accepted(T),
Rejected { error_code: i32 },
InconsistentStatus { success: bool, error_code: i32 },
}
impl<'de, T> Deserialize<'de> for Envelope<T>
where
T: serde::de::DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let mut object = BTreeMap::<String, Box<RawValue>>::deserialize(deserializer)?;
let success = object
.remove("success")
.ok_or_else(|| D::Error::custom("provider response success flag is missing"))
.and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
let error_code = object
.remove("errorCode")
.ok_or_else(|| D::Error::custom("provider response error code is missing"))
.and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
object.remove("errorMessage");
if success != (error_code == 0) {
return Ok(Self::InconsistentStatus {
success,
error_code,
});
}
if !success {
return Ok(Self::Rejected { error_code });
}
let mut body_json = String::from("{");
for (index, (key, value)) in object.into_iter().enumerate() {
if index > 0 {
body_json.push(',');
}
body_json.push_str(&serde_json::to_string(&key).map_err(D::Error::custom)?);
body_json.push(':');
body_json.push_str(value.get());
}
body_json.push('}');
let body = serde_json::from_str(&body_json).map_err(D::Error::custom)?;
Ok(Self::Accepted(body))
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct AccountsBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) accounts: Vec<Account>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ContractsBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) contracts: Vec<Contract>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ContractBody {
pub(crate) contract: Contract,
}
#[derive(Debug, Deserialize)]
pub(crate) struct BarsBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) bars: Vec<Bar>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OrdersBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) orders: Vec<Order>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OrderBody {
pub(crate) order: Order,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaceOrderBody {
pub(crate) order_id: Option<OrderId>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct PositionsBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) positions: Vec<Position>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct TradesBody {
#[serde(default, deserialize_with = "null_to_empty")]
pub(crate) trades: Vec<Trade>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EmptyBody {}
pub(crate) fn null_to_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_empty_list {
($body:ty, $field:ident, $json:literal) => {{
let envelope: Envelope<$body> = serde_json::from_str($json)
.unwrap_or_else(|error| panic!("fixture envelope must decode: {error}"));
let Envelope::Accepted(body) = envelope else {
panic!("fixture envelope must be accepted");
};
assert!(body.$field.is_empty());
}};
}
#[test]
fn optional_list_bodies_normalize_missing_and_null_to_empty() {
assert_empty_list!(
AccountsBody,
accounts,
r#"{"success":true,"errorCode":0,"accounts":null}"#
);
assert_empty_list!(
ContractsBody,
contracts,
r#"{"success":true,"errorCode":0}"#
);
assert_empty_list!(
OrdersBody,
orders,
r#"{"success":true,"errorCode":0,"orders":null}"#
);
assert_empty_list!(
OrderPage,
orders,
r#"{"success":true,"errorCode":0,"orders":null}"#
);
assert_empty_list!(
PositionsBody,
positions,
r#"{"success":true,"errorCode":0}"#
);
assert_empty_list!(
TradesBody,
trades,
r#"{"success":true,"errorCode":0,"trades":null}"#
);
}
#[test]
fn rejected_envelope_does_not_require_an_endpoint_body() {
let envelope: Envelope<AccountsBody> =
serde_json::from_str(r#"{"success":false,"errorCode":17,"errorMessage":"synthetic"}"#)
.unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
assert!(matches!(envelope, Envelope::Rejected { error_code: 17 }));
}
#[test]
fn envelope_requires_a_consistent_provider_status() {
assert!(
serde_json::from_str::<Envelope<AccountsBody>>(r#"{"success":true,"accounts":[]}"#)
.is_err()
);
for (json, success, error_code) in [
(r#"{"success":true,"errorCode":17}"#, true, 17),
(r#"{"success":false,"errorCode":0}"#, false, 0),
] {
let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
.unwrap_or_else(|error| panic!("inconsistent envelope must decode: {error}"));
assert!(matches!(
envelope,
Envelope::InconsistentStatus {
success: actual_success,
error_code: actual_error_code,
} if actual_success == success && actual_error_code == error_code
));
}
}
}