use num_enum::{FromPrimitive, IntoPrimitive};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use strum_macros::{Display, EnumString};
use time::{Date, OffsetDateTime};
use crate::{serde_utils, Market};
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
#[allow(clippy::upper_case_acronyms)]
pub enum OrderType {
#[strum(disabled)]
Unknown,
#[strum(serialize = "LO")]
LO,
#[strum(serialize = "ELO")]
ELO,
#[strum(serialize = "MO")]
MO,
#[strum(serialize = "AO")]
AO,
#[strum(serialize = "ALO")]
ALO,
#[strum(serialize = "ODD")]
ODD,
#[strum(serialize = "LIT")]
LIT,
#[strum(serialize = "MIT")]
MIT,
#[strum(serialize = "TSLPAMT")]
TSLPAMT,
#[strum(serialize = "TSLPPCT")]
TSLPPCT,
#[strum(serialize = "TSMAMT")]
TSMAMT,
#[strum(serialize = "TSMPCT")]
TSMPCT,
#[strum(serialize = "SLO")]
SLO,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum OrderStatus {
#[strum(disabled)]
Unknown,
#[strum(serialize = "NotReported")]
NotReported,
#[strum(serialize = "ReplacedNotReported")]
ReplacedNotReported,
#[strum(serialize = "ProtectedNotReported")]
ProtectedNotReported,
#[strum(serialize = "VarietiesNotReported")]
VarietiesNotReported,
#[strum(serialize = "FilledStatus")]
Filled,
#[strum(serialize = "WaitToNew")]
WaitToNew,
#[strum(serialize = "NewStatus")]
New,
#[strum(serialize = "WaitToReplace")]
WaitToReplace,
#[strum(serialize = "PendingReplaceStatus")]
PendingReplace,
#[strum(serialize = "ReplacedStatus")]
Replaced,
#[strum(serialize = "PartialFilledStatus")]
PartialFilled,
#[strum(serialize = "WaitToCancel")]
WaitToCancel,
#[strum(serialize = "PendingCancelStatus")]
PendingCancel,
#[strum(serialize = "RejectedStatus")]
Rejected,
#[strum(serialize = "CanceledStatus")]
Canceled,
#[strum(serialize = "ExpiredStatus")]
Expired,
#[strum(serialize = "PartialWithdrawal")]
PartialWithdrawal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Execution {
pub order_id: String,
pub trade_id: String,
pub symbol: String,
#[serde(with = "serde_utils::timestamp")]
pub trade_done_at: OffsetDateTime,
pub quantity: Decimal,
pub price: Decimal,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum OrderSide {
#[strum(disabled)]
Unknown,
#[strum(serialize = "Buy")]
Buy,
#[strum(serialize = "Sell")]
Sell,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum TriggerPriceType {
#[strum(disabled)]
Unknown,
#[strum(serialize = "LIT")]
LimitIfTouched,
#[strum(serialize = "MIT")]
MarketIfTouched,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum OrderTag {
#[strum(disabled)]
Unknown,
#[strum(serialize = "Normal")]
Normal,
#[strum(serialize = "GTC")]
LongTerm,
#[strum(serialize = "Grey")]
Grey,
MarginCall,
Offline,
Creditor,
Debtor,
NonExercise,
AllocatedSub,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum TimeInForceType {
#[strum(disabled)]
Unknown,
#[strum(serialize = "Day")]
Day,
#[strum(serialize = "GTC")]
GoodTilCanceled,
#[strum(serialize = "GTD")]
GoodTilDate,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum TriggerStatus {
#[strum(disabled)]
Unknown,
#[strum(serialize = "DEACTIVE")]
Deactive,
#[strum(serialize = "ACTIVE")]
Active,
#[strum(serialize = "RELEASED")]
Released,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum OutsideRTH {
#[strum(disabled)]
Unknown,
#[strum(serialize = "RTH_ONLY")]
RTHOnly,
#[strum(serialize = "ANY_TIME")]
AnyTime,
#[strum(serialize = "OVERNIGHT")]
Overnight,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Order {
pub order_id: String,
pub status: OrderStatus,
pub stock_name: String,
pub quantity: Decimal,
pub executed_quantity: Decimal,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub price: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_0_is_none")]
pub executed_price: Option<Decimal>,
#[serde(with = "serde_utils::timestamp")]
pub submitted_at: OffsetDateTime,
pub side: OrderSide,
pub symbol: String,
pub order_type: OrderType,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub last_done: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trigger_price: Option<Decimal>,
pub msg: String,
pub tag: OrderTag,
pub time_in_force: TimeInForceType,
#[serde(with = "serde_utils::date_opt")]
pub expire_date: Option<Date>,
#[serde(with = "serde_utils::timestamp_opt")]
pub updated_at: Option<OffsetDateTime>,
#[serde(with = "serde_utils::timestamp_opt")]
pub trigger_at: Option<OffsetDateTime>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trailing_amount: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trailing_percent: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub limit_offset: Option<Decimal>,
#[serde(with = "serde_utils::trigger_status")]
pub trigger_status: Option<TriggerStatus>,
pub currency: String,
#[serde(with = "serde_utils::outside_rth")]
pub outside_rth: Option<OutsideRTH>,
pub remark: String,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum CommissionFreeStatus {
#[strum(disabled)]
Unknown,
None,
Calculated,
Pending,
Ready,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum DeductionStatus {
#[strum(disabled)]
Unknown,
None,
#[strum(serialize = "NO_DATA")]
NoData,
#[strum(serialize = "PENDING")]
Pending,
#[strum(serialize = "DONE")]
Done,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum ChargeCategoryCode {
#[strum(disabled)]
Unknown,
#[strum(serialize = "BROKER_FEES")]
Broker,
#[strum(serialize = "THIRD_FEES")]
Third,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderHistoryDetail {
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub price: Decimal,
pub quantity: Decimal,
pub status: OrderStatus,
pub msg: String,
#[serde(with = "serde_utils::timestamp")]
pub time: OffsetDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderChargeFee {
pub code: String,
pub name: String,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub amount: Decimal,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderChargeItem {
pub code: ChargeCategoryCode,
pub name: String,
pub fees: Vec<OrderChargeFee>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderChargeDetail {
pub total_amount: Decimal,
pub currency: String,
pub items: Vec<OrderChargeItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderDetail {
pub order_id: String,
pub status: OrderStatus,
pub stock_name: String,
pub quantity: Decimal,
pub executed_quantity: Decimal,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub price: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_0_is_none")]
pub executed_price: Option<Decimal>,
#[serde(with = "serde_utils::timestamp")]
pub submitted_at: OffsetDateTime,
pub side: OrderSide,
pub symbol: String,
pub order_type: OrderType,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub last_done: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trigger_price: Option<Decimal>,
pub msg: String,
pub tag: OrderTag,
pub time_in_force: TimeInForceType,
#[serde(with = "serde_utils::date_opt")]
pub expire_date: Option<Date>,
#[serde(with = "serde_utils::timestamp_opt")]
pub updated_at: Option<OffsetDateTime>,
#[serde(with = "serde_utils::timestamp_opt")]
pub trigger_at: Option<OffsetDateTime>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trailing_amount: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub trailing_percent: Option<Decimal>,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub limit_offset: Option<Decimal>,
#[serde(with = "serde_utils::trigger_status")]
pub trigger_status: Option<TriggerStatus>,
pub currency: String,
#[serde(with = "serde_utils::outside_rth")]
pub outside_rth: Option<OutsideRTH>,
pub remark: String,
pub free_status: CommissionFreeStatus,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub free_amount: Option<Decimal>,
#[serde(with = "serde_utils::symbol_opt")]
pub free_currency: Option<String>,
pub deductions_status: DeductionStatus,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub deductions_amount: Option<Decimal>,
#[serde(with = "serde_utils::symbol_opt")]
pub deductions_currency: Option<String>,
pub platform_deducted_status: DeductionStatus,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub platform_deducted_amount: Option<Decimal>,
#[serde(with = "serde_utils::symbol_opt")]
pub platform_deducted_currency: Option<String>,
pub history: Vec<OrderHistoryDetail>,
pub charge_detail: OrderChargeDetail,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashInfo {
pub withdraw_cash: Decimal,
pub available_cash: Decimal,
pub frozen_cash: Decimal,
pub settling_cash: Decimal,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountBalance {
pub total_cash: Decimal,
pub max_finance_amount: Decimal,
pub remaining_finance_amount: Decimal,
#[serde(with = "serde_utils::risk_level")]
pub risk_level: i32,
pub margin_call: Decimal,
pub currency: String,
#[serde(default)]
pub cash_infos: Vec<CashInfo>,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub net_assets: Decimal,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub init_margin: Decimal,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub maintenance_margin: Decimal,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub buy_power: Decimal,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
#[repr(i32)]
pub enum BalanceType {
#[num_enum(default)]
Unknown = 0,
Cash = 1,
Stock = 2,
Fund = 3,
}
impl Serialize for BalanceType {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let value: i32 = (*self).into();
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for BalanceType {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = i32::deserialize(deserializer)?;
Ok(BalanceType::from(value))
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, Serialize)]
#[repr(i32)]
pub enum CashFlowDirection {
#[num_enum(default)]
Unknown,
Out = 1,
In = 2,
}
impl<'de> Deserialize<'de> for CashFlowDirection {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = i32::deserialize(deserializer)?;
Ok(CashFlowDirection::from(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashFlow {
pub transaction_flow_name: String,
pub direction: CashFlowDirection,
pub business_type: BalanceType,
pub balance: Decimal,
pub currency: String,
#[serde(with = "serde_utils::timestamp")]
pub business_time: OffsetDateTime,
#[serde(with = "serde_utils::symbol_opt")]
pub symbol: Option<String>,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundPositionsResponse {
#[serde(rename = "list")]
pub channels: Vec<FundPositionChannel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundPositionChannel {
pub account_channel: String,
#[serde(default, rename = "fund_info")]
pub positions: Vec<FundPosition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundPosition {
pub symbol: String,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub current_net_asset_value: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub net_asset_value_day: OffsetDateTime,
pub symbol_name: String,
pub currency: String,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub cost_net_asset_value: Decimal,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub holding_units: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockPositionsResponse {
#[serde(rename = "list")]
pub channels: Vec<StockPositionChannel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockPositionChannel {
pub account_channel: String,
#[serde(default, rename = "stock_info")]
pub positions: Vec<StockPosition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockPosition {
pub symbol: String,
pub symbol_name: String,
pub quantity: Decimal,
pub available_quantity: Decimal,
pub currency: String,
#[serde(with = "serde_utils::decimal_empty_is_0")]
pub cost_price: Decimal,
pub market: Market,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub init_quantity: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarginRatio {
pub im_factor: Decimal,
pub mm_factor: Decimal,
pub fm_factor: Decimal,
}
impl_serde_for_enum_string!(
OrderType,
OrderStatus,
OrderSide,
TriggerPriceType,
OrderTag,
TimeInForceType,
TriggerStatus,
OutsideRTH,
CommissionFreeStatus,
DeductionStatus,
ChargeCategoryCode
);
impl_default_for_enum_string!(
OrderType,
OrderStatus,
OrderSide,
TriggerPriceType,
OrderTag,
TimeInForceType,
TriggerStatus,
OutsideRTH,
CommissionFreeStatus,
DeductionStatus,
ChargeCategoryCode
);
#[cfg(test)]
mod tests {
use time::macros::datetime;
use super::*;
#[test]
fn fund_position_response() {
let data = r#"
{
"list": [{
"account_channel": "lb",
"fund_info": [{
"symbol": "HK0000447943",
"symbol_name": "高腾亚洲收益基金",
"currency": "USD",
"holding_units": "5.000",
"current_net_asset_value": "0",
"cost_net_asset_value": "0.00",
"net_asset_value_day": "1649865600"
}]
}]
}
"#;
let resp: FundPositionsResponse = serde_json::from_str(data).unwrap();
assert_eq!(resp.channels.len(), 1);
let channel = &resp.channels[0];
assert_eq!(channel.account_channel, "lb");
assert_eq!(channel.positions.len(), 1);
let position = &channel.positions[0];
assert_eq!(position.symbol, "HK0000447943");
assert_eq!(position.symbol_name, "高腾亚洲收益基金");
assert_eq!(position.currency, "USD");
assert_eq!(position.current_net_asset_value, decimal!(0i32));
assert_eq!(position.cost_net_asset_value, decimal!(0i32));
assert_eq!(position.holding_units, decimal!(5i32));
assert_eq!(position.net_asset_value_day, datetime!(2022-4-14 0:00 +8));
}
#[test]
fn stock_position_response() {
let data = r#"
{
"list": [
{
"account_channel": "lb",
"stock_info": [
{
"symbol": "700.HK",
"symbol_name": "腾讯控股",
"currency": "HK",
"quantity": "650",
"available_quantity": "-450",
"cost_price": "457.53",
"market": "HK",
"init_quantity": "2000"
},
{
"symbol": "9991.HK",
"symbol_name": "宝尊电商-SW",
"currency": "HK",
"quantity": "200",
"available_quantity": "0",
"cost_price": "32.25",
"market": "HK",
"init_quantity": ""
}
]
}
]
}
"#;
let resp: StockPositionsResponse = serde_json::from_str(data).unwrap();
assert_eq!(resp.channels.len(), 1);
let channel = &resp.channels[0];
assert_eq!(channel.account_channel, "lb");
assert_eq!(channel.positions.len(), 2);
let position = &channel.positions[0];
assert_eq!(position.symbol, "700.HK");
assert_eq!(position.symbol_name, "腾讯控股");
assert_eq!(position.currency, "HK");
assert_eq!(position.quantity, decimal!(650));
assert_eq!(position.available_quantity, decimal!(-450));
assert_eq!(position.cost_price, decimal!(457.53f32));
assert_eq!(position.market, Market::HK);
assert_eq!(position.init_quantity, Some(decimal!(2000)));
let position = &channel.positions[0];
assert_eq!(position.symbol, "700.HK");
assert_eq!(position.symbol_name, "腾讯控股");
assert_eq!(position.currency, "HK");
assert_eq!(position.quantity, decimal!(650));
assert_eq!(position.available_quantity, decimal!(-450));
assert_eq!(position.cost_price, decimal!(457.53f32));
assert_eq!(position.market, Market::HK);
let position = &channel.positions[1];
assert_eq!(position.symbol, "9991.HK");
assert_eq!(position.symbol_name, "宝尊电商-SW");
assert_eq!(position.currency, "HK");
assert_eq!(position.quantity, decimal!(200));
assert_eq!(position.available_quantity, decimal!(0));
assert_eq!(position.cost_price, decimal!(32.25f32));
assert_eq!(position.init_quantity, None);
}
#[test]
fn cash_flow() {
let data = r#"
{
"list": [
{
"transaction_flow_name": "BuyContract-Stocks",
"direction": 1,
"balance": "-248.60",
"currency": "USD",
"business_type": 1,
"business_time": "1621507957",
"symbol": "AAPL.US",
"description": "AAPL"
},
{
"transaction_flow_name": "BuyContract-Stocks",
"direction": 1,
"balance": "-125.16",
"currency": "USD",
"business_type": 2,
"business_time": "1621504824",
"symbol": "AAPL.US",
"description": "AAPL"
}
]
}
"#;
#[derive(Debug, Deserialize)]
struct Response {
list: Vec<CashFlow>,
}
let resp: Response = serde_json::from_str(data).unwrap();
assert_eq!(resp.list.len(), 2);
let cashflow = &resp.list[0];
assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
assert_eq!(cashflow.direction, CashFlowDirection::Out);
assert_eq!(cashflow.balance, decimal!(-248.60f32));
assert_eq!(cashflow.currency, "USD");
assert_eq!(cashflow.business_type, BalanceType::Cash);
assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:52:37 +8));
assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
assert_eq!(cashflow.description, "AAPL");
let cashflow = &resp.list[1];
assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
assert_eq!(cashflow.direction, CashFlowDirection::Out);
assert_eq!(cashflow.balance, decimal!(-125.16f32));
assert_eq!(cashflow.currency, "USD");
assert_eq!(cashflow.business_type, BalanceType::Stock);
assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:00:24 +8));
assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
assert_eq!(cashflow.description, "AAPL");
}
#[test]
fn account_balance() {
let data = r#"
{
"list": [
{
"total_cash": "1759070010.72",
"max_finance_amount": "977582000",
"remaining_finance_amount": "0",
"risk_level": "1",
"margin_call": "2598051051.50",
"currency": "HKD",
"cash_infos": [
{
"withdraw_cash": "97592.30",
"available_cash": "195902464.37",
"frozen_cash": "11579339.13",
"settling_cash": "207288537.81",
"currency": "HKD"
},
{
"withdraw_cash": "199893416.74",
"available_cash": "199893416.74",
"frozen_cash": "28723.76",
"settling_cash": "-276806.51",
"currency": "USD"
}
],
"net_assets": "11111.12",
"init_margin": "2222.23",
"maintenance_margin": "3333.45",
"buy_power": "1234.67"
}
]
}"#;
#[derive(Debug, Deserialize)]
struct Response {
list: Vec<AccountBalance>,
}
let resp: Response = serde_json::from_str(data).unwrap();
assert_eq!(resp.list.len(), 1);
let balance = &resp.list[0];
assert_eq!(balance.total_cash, "1759070010.72".parse().unwrap());
assert_eq!(balance.max_finance_amount, "977582000".parse().unwrap());
assert_eq!(balance.remaining_finance_amount, decimal!(0i32));
assert_eq!(balance.risk_level, 1);
assert_eq!(balance.margin_call, "2598051051.50".parse().unwrap());
assert_eq!(balance.currency, "HKD");
assert_eq!(balance.net_assets, "11111.12".parse().unwrap());
assert_eq!(balance.init_margin, "2222.23".parse().unwrap());
assert_eq!(balance.maintenance_margin, "3333.45".parse().unwrap());
assert_eq!(balance.buy_power, "1234.67".parse().unwrap());
assert_eq!(balance.cash_infos.len(), 2);
let cash_info = &balance.cash_infos[0];
assert_eq!(cash_info.withdraw_cash, "97592.30".parse().unwrap());
assert_eq!(cash_info.available_cash, "195902464.37".parse().unwrap());
assert_eq!(cash_info.frozen_cash, "11579339.13".parse().unwrap());
assert_eq!(cash_info.settling_cash, "207288537.81".parse().unwrap());
assert_eq!(cash_info.currency, "HKD");
let cash_info = &balance.cash_infos[1];
assert_eq!(cash_info.withdraw_cash, "199893416.74".parse().unwrap());
assert_eq!(cash_info.available_cash, "199893416.74".parse().unwrap());
assert_eq!(cash_info.frozen_cash, "28723.76".parse().unwrap());
assert_eq!(cash_info.settling_cash, "-276806.51".parse().unwrap());
assert_eq!(cash_info.currency, "USD");
}
#[test]
fn history_orders() {
let data = r#"
{
"orders": [
{
"currency": "HKD",
"executed_price": "0.000",
"executed_quantity": "0",
"expire_date": "",
"last_done": "",
"limit_offset": "",
"msg": "",
"order_id": "706388312699592704",
"order_type": "ELO",
"outside_rth": "UnknownOutsideRth",
"price": "11.900",
"quantity": "200",
"side": "Buy",
"status": "RejectedStatus",
"stock_name": "Bank of East Asia Ltd/The",
"submitted_at": "1651644897",
"symbol": "23.HK",
"tag": "Normal",
"time_in_force": "Day",
"trailing_amount": "",
"trailing_percent": "",
"trigger_at": "0",
"trigger_price": "",
"trigger_status": "NOT_USED",
"updated_at": "1651644898",
"remark": "abc"
}
]
}
"#;
#[derive(Deserialize)]
struct Response {
orders: Vec<Order>,
}
let resp: Response = serde_json::from_str(data).unwrap();
assert_eq!(resp.orders.len(), 1);
let order = &resp.orders[0];
assert_eq!(order.currency, "HKD");
assert!(order.executed_price.is_none());
assert_eq!(order.executed_quantity, decimal!(0));
assert!(order.expire_date.is_none());
assert!(order.last_done.is_none());
assert!(order.limit_offset.is_none());
assert_eq!(order.msg, "");
assert_eq!(order.order_id, "706388312699592704");
assert_eq!(order.order_type, OrderType::ELO);
assert!(order.outside_rth.is_none());
assert_eq!(order.price, Some("11.900".parse().unwrap()));
assert_eq!(order.quantity, decimal!(200));
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.status, OrderStatus::Rejected);
assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
assert_eq!(order.symbol, "23.HK");
assert_eq!(order.tag, OrderTag::Normal);
assert_eq!(order.time_in_force, TimeInForceType::Day);
assert!(order.trailing_amount.is_none());
assert!(order.trailing_percent.is_none());
assert!(order.trigger_at.is_none());
assert!(order.trigger_price.is_none());
assert!(order.trigger_status.is_none());
assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
assert_eq!(order.remark, "abc");
}
#[test]
fn today_orders() {
let data = r#"
{
"orders": [
{
"currency": "HKD",
"executed_price": "0.000",
"executed_quantity": "0",
"expire_date": "",
"last_done": "",
"limit_offset": "",
"msg": "",
"order_id": "706388312699592704",
"order_type": "ELO",
"outside_rth": "UnknownOutsideRth",
"price": "11.900",
"quantity": "200",
"side": "Buy",
"status": "RejectedStatus",
"stock_name": "Bank of East Asia Ltd/The",
"submitted_at": "1651644897",
"symbol": "23.HK",
"tag": "Normal",
"time_in_force": "Day",
"trailing_amount": "",
"trailing_percent": "",
"trigger_at": "0",
"trigger_price": "",
"trigger_status": "NOT_USED",
"updated_at": "1651644898",
"remark": "abc"
}
]
}
"#;
#[derive(Deserialize)]
struct Response {
orders: Vec<Order>,
}
let resp: Response = serde_json::from_str(data).unwrap();
assert_eq!(resp.orders.len(), 1);
let order = &resp.orders[0];
assert_eq!(order.currency, "HKD");
assert!(order.executed_price.is_none());
assert_eq!(order.executed_quantity, decimal!(0));
assert!(order.expire_date.is_none());
assert!(order.last_done.is_none());
assert!(order.limit_offset.is_none());
assert_eq!(order.msg, "");
assert_eq!(order.order_id, "706388312699592704");
assert_eq!(order.order_type, OrderType::ELO);
assert!(order.outside_rth.is_none());
assert_eq!(order.price, Some("11.900".parse().unwrap()));
assert_eq!(order.quantity, decimal!(200));
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.status, OrderStatus::Rejected);
assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
assert_eq!(order.symbol, "23.HK");
assert_eq!(order.tag, OrderTag::Normal);
assert_eq!(order.time_in_force, TimeInForceType::Day);
assert!(order.trailing_amount.is_none());
assert!(order.trailing_percent.is_none());
assert!(order.trigger_at.is_none());
assert!(order.trigger_price.is_none());
assert!(order.trigger_status.is_none());
assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
assert_eq!(order.remark, "abc");
}
#[test]
fn history_executions() {
let data = r#"
{
"has_more": false,
"trades": [
{
"order_id": "693664675163312128",
"price": "388",
"quantity": "100",
"symbol": "700.HK",
"trade_done_at": "1648611351",
"trade_id": "693664675163312128-1648611351433741210"
}
]
}
"#;
#[derive(Deserialize)]
struct Response {
trades: Vec<Execution>,
}
let resp: Response = serde_json::from_str(data).unwrap();
assert_eq!(resp.trades.len(), 1);
let execution = &resp.trades[0];
assert_eq!(execution.order_id, "693664675163312128");
assert_eq!(execution.price, "388".parse().unwrap());
assert_eq!(execution.quantity, decimal!(100));
assert_eq!(execution.symbol, "700.HK");
assert_eq!(execution.trade_done_at, datetime!(2022-03-30 11:35:51 +8));
assert_eq!(execution.trade_id, "693664675163312128-1648611351433741210");
}
#[test]
fn order_detail() {
let data = r#"
{
"order_id": "828940451093708800",
"status": "FilledStatus",
"stock_name": "Apple",
"quantity": "10",
"executed_quantity": "10",
"price": "200.000",
"executed_price": "164.660",
"submitted_at": "1680863604",
"side": "Buy",
"symbol": "AAPL.US",
"order_type": "LO",
"last_done": "164.660",
"trigger_price": "0.0000",
"msg": "",
"tag": "Normal",
"time_in_force": "Day",
"expire_date": "2023-04-10",
"updated_at": "1681113000",
"trigger_at": "0",
"trailing_amount": "",
"trailing_percent": "",
"limit_offset": "",
"trigger_status": "NOT_USED",
"outside_rth": "ANY_TIME",
"currency": "USD",
"remark": "1680863603.927165",
"free_status": "None",
"free_amount": "",
"free_currency": "",
"deductions_status": "NONE",
"deductions_amount": "",
"deductions_currency": "",
"platform_deducted_status": "NONE",
"platform_deducted_amount": "",
"platform_deducted_currency": "",
"history": [{
"price": "164.6600",
"quantity": "10",
"status": "FilledStatus",
"msg": "Execution of 10",
"time": "1681113000"
}, {
"price": "200.0000",
"quantity": "10",
"status": "NewStatus",
"msg": "",
"time": "1681113000"
}],
"charge_detail": {
"items": [{
"code": "BROKER_FEES",
"name": "Broker Fees",
"fees": []
}, {
"code": "THIRD_FEES",
"name": "Third-party Fees",
"fees": []
}],
"total_amount": "0",
"currency": "USD"
}
}
"#;
_ = serde_json::from_str::<OrderDetail>(data).unwrap();
}
}