use crate::wire::{
DepthMarketData, ErrorField, InputOrderAction, InstrumentField, InvestorPositionField,
OrderField, TradeField, TradingAccountField,
};
use ek_data::account::AccountData;
use ek_data::constant::{Asset, Direction, Exchange, InstrumentType, Offset, OrderType, Status};
use ek_data::contract::ContractData;
use ek_data::order::OrderData;
use ek_data::tick::TickData;
use ek_data::time::DtNs;
use ek_data::trade::TradeData;
use std::time::{SystemTime, UNIX_EPOCH};
pub const MAX_FLOAT: f64 = f64::MAX;
const OST_ALL_TRADED: &str = "0";
const OST_PART_TRADED_Q: &str = "1";
const OST_NO_TRADE_Q: &str = "3";
const OST_PART_TRADED_NQ: &str = "2";
const OST_NO_TRADE_NQ: &str = "4";
const OST_CANCELED: &str = "5";
const OST_UNKNOWN: &str = "a";
const D_BUY: &str = "0";
const D_SELL: &str = "1";
#[allow(dead_code)]
const PD_NET: &str = "1";
const PD_LONG: &str = "2";
const PD_SHORT: &str = "3";
const OFEN_OPEN: &str = "0";
const OFEN_CLOSE: &str = "1";
const OFEN_CLOSE_TODAY: &str = "3";
const OFEN_CLOSE_YESTERDAY: &str = "4";
const OPT_ANY_PRICE: &str = "1";
const OPT_LIMIT_PRICE: &str = "2";
const TC_IOC: &str = "1";
const TC_GFD: &str = "3";
const VC_AV: &str = "1";
const VC_CV: &str = "3";
const PC_FUTURES: &str = "1";
const PC_OPTIONS: &str = "2";
const PC_COMBINATION: &str = "3";
const PC_SPOT_OPTION: &str = "6";
const PC_TAS: &str = "7";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProductClasses(pub Vec<String>);
impl ProductClasses {
pub fn default_classes() -> Vec<String> {
vec![PC_FUTURES.to_string()]
}
pub fn normalize(&self) -> Vec<String> {
let mut out: Vec<String> = self
.0
.iter()
.map(|c| c.trim().to_string())
.filter(|c| c.chars().count() == 1 && !c.is_empty())
.collect();
out.sort();
out.dedup();
if out.is_empty() {
return Self::default_classes();
}
out
}
pub fn accepts(&self, product_class: &str) -> bool {
self.normalize().iter().any(|c| c == product_class)
}
}
pub fn exchange_of(exchange_id: &str) -> Option<Exchange> {
match exchange_id {
"CFFEX" => Some(Exchange::CFFEX),
"SHFE" => Some(Exchange::SHFE),
"CZCE" => Some(Exchange::CZCE),
"DCE" => Some(Exchange::DCE),
"INE" => Some(Exchange::INE),
"GFEX" => Some(Exchange::GFEX),
_ => None,
}
}
pub fn asset_from(product_class: &str) -> Option<Asset> {
match product_class {
PC_FUTURES | PC_COMBINATION | PC_TAS => Some(Asset::FUTURES),
PC_OPTIONS | PC_SPOT_OPTION => Some(Asset::OPTION),
_ => None,
}
}
pub fn instrument_type_from(product_class: &str) -> Option<InstrumentType> {
match product_class {
PC_COMBINATION => Some(InstrumentType::SPREAD),
PC_TAS => Some(InstrumentType::FUTURES_CONTRACT),
_ => None,
}
}
pub fn status_of(code: &str) -> Option<Status> {
match code {
OST_NO_TRADE_Q => Some(Status::NOTTRADED),
OST_PART_TRADED_Q => Some(Status::PARTTRADED),
OST_ALL_TRADED => Some(Status::ALLTRADED),
OST_PART_TRADED_NQ => Some(Status::PARTTRADED),
OST_NO_TRADE_NQ => Some(Status::NOTTRADED),
OST_CANCELED => Some(Status::CANCELLED),
OST_UNKNOWN => Some(Status::SUBMITTING),
_ => None,
}
}
pub fn direction_from(code: &str) -> Option<Direction> {
match code {
D_BUY | PD_LONG => Some(Direction::LONG),
D_SELL | PD_SHORT => Some(Direction::SHORT),
_ => None,
}
}
pub fn offset_from(code: &str) -> Option<Offset> {
match code {
OFEN_OPEN => Some(Offset::OPEN),
OFEN_CLOSE => Some(Offset::CLOSE),
OFEN_CLOSE_TODAY => Some(Offset::CLOSETODAY),
OFEN_CLOSE_YESTERDAY => Some(Offset::CLOSEYESTERDAY),
_ => None,
}
}
pub fn order_type_from(price_type: &str, time_cond: &str, volume_cond: &str) -> Option<OrderType> {
match (price_type, time_cond, volume_cond) {
(OPT_LIMIT_PRICE, TC_GFD, VC_AV) => Some(OrderType::LIMIT),
(OPT_ANY_PRICE, TC_GFD, VC_AV) => Some(OrderType::MARKET),
(OPT_LIMIT_PRICE, TC_IOC, VC_AV) => Some(OrderType::FAK),
(OPT_LIMIT_PRICE, TC_IOC, VC_CV) => Some(OrderType::FOK),
_ => None,
}
}
pub fn direction_to(d: Direction) -> Option<&'static str> {
match d {
Direction::LONG => Some(D_BUY),
Direction::SHORT => Some(D_SELL),
_ => None,
}
}
pub fn offset_to(exchange: Exchange, o: Offset) -> Option<&'static str> {
let o = match o {
Offset::CLOSETODAY | Offset::CLOSEYESTERDAY
if exchange != Exchange::SHFE && exchange != Exchange::INE =>
{
Offset::CLOSE
}
other => other,
};
match o {
Offset::OPEN => Some(OFEN_OPEN),
Offset::CLOSE => Some(OFEN_CLOSE),
Offset::CLOSETODAY => Some(OFEN_CLOSE_TODAY),
Offset::CLOSEYESTERDAY => Some(OFEN_CLOSE_YESTERDAY),
_ => None,
}
}
pub fn order_type_to(t: OrderType) -> Option<(&'static str, &'static str, &'static str)> {
match t {
OrderType::LIMIT => Some((OPT_LIMIT_PRICE, TC_GFD, VC_AV)),
OrderType::MARKET => Some((OPT_ANY_PRICE, TC_GFD, VC_AV)),
OrderType::FAK => Some((OPT_LIMIT_PRICE, TC_IOC, VC_AV)),
OrderType::FOK => Some((OPT_LIMIT_PRICE, TC_IOC, VC_CV)),
_ => None,
}
}
pub fn adjust_price(price: f64) -> f64 {
if price == MAX_FLOAT { 0.0 } else { price }
}
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
pub fn ctp_datetime(date: &str, time: &str, millis: i64) -> Option<DtNs> {
if date.len() != 8 || time.len() != 8 || !date.is_ascii() || !time.is_ascii() {
return None;
}
let digits_ok = |s: &str| s.bytes().all(|b: u8| b.is_ascii_digit());
if !(digits_ok(date)
&& digits_ok(&time[..2])
&& digits_ok(&time[3..5])
&& digits_ok(&time[6..8]))
{
return None;
}
let (y, m, d) = (
date[0..4].parse::<i64>().ok()?,
date[4..6].parse::<i64>().ok()?,
date[6..8].parse::<i64>().ok()?,
);
let (h, mi, s) = (
time[0..2].parse::<i64>().ok()?,
time[3..5].parse::<i64>().ok()?,
time[6..8].parse::<i64>().ok()?,
);
if !((1..=12).contains(&m) && h < 24 && mi < 60 && s < 60) {
return None;
}
let secs = days_from_civil(y, m, d)
.checked_mul(86_400)?
.checked_add(h * 3_600 + mi * 60 + s - 8 * 3_600)?;
secs.checked_mul(1_000_000_000)?
.checked_add(millis.checked_mul(1_000_000)?)
}
#[must_use]
pub fn now_epoch_ms() -> i64 {
ek_data::time::now_nanos() / 1_000_000
}
pub fn shanghai_now_date() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
+ 8 * 3_600;
let days = secs.div_euclid(86_400);
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}{m:02}{d:02}")
}
pub fn error_text(msg: &str, e: &ErrorField) -> String {
format!("{},代码:{},信息:{}", msg, e.error_id, e.error_msg)
}
fn yyyymmdd_i32(s: &str) -> Option<i32> {
if s.len() != 8 || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match s.parse::<i32>() {
Ok(0) => None,
Ok(v) => Some(v),
Err(_) => None,
}
}
pub fn tick_from(
dm: &DepthMarketData,
contract: Option<&ContractData>,
gw: &str,
current_date: &str,
) -> TickData {
let exchange = match contract {
Some(c) => c.exchange,
None => exchange_of(&dm.exchange_id).unwrap_or(Exchange::UNKNOWN),
};
let date_str = if dm.action_day.is_empty() || exchange == Exchange::DCE {
current_date.to_string()
} else {
dm.action_day.clone()
};
let dt = ctp_datetime(&date_str, &dm.update_time, dm.update_millisec);
let trading_day = yyyymmdd_i32(&dm.trading_day);
let action_day = yyyymmdd_i32(&date_str);
let mut tick = TickData {
gateway_name: gw.to_string(),
symbol: dm.instrument_id.clone(),
exchange,
datetime: dt.unwrap_or_default(),
datetime_invalid: dt.is_none(),
trading_day,
action_day,
update_time: dm.update_time.clone(),
update_millisec: dm.update_millisec,
name: contract.map(|c| c.name.clone()).unwrap_or_default(),
volume: dm.volume,
turnover: dm.turnover,
open_interest: dm.open_interest,
last_price: adjust_price(dm.last_price),
limit_up: adjust_price(dm.upper_limit_price),
limit_down: adjust_price(dm.lower_limit_price),
open_price: adjust_price(dm.open_price),
high_price: adjust_price(dm.highest_price),
low_price: adjust_price(dm.lowest_price),
pre_close: adjust_price(dm.pre_close_price),
bid_price_1: adjust_price(dm.bid_price1),
ask_price_1: adjust_price(dm.ask_price1),
bid_volume_1: dm.bid_volume1,
ask_volume_1: dm.ask_volume1,
bid_price_2: 0.0,
bid_price_3: 0.0,
bid_price_4: 0.0,
bid_price_5: 0.0,
ask_price_2: 0.0,
ask_price_3: 0.0,
ask_price_4: 0.0,
ask_price_5: 0.0,
bid_volume_2: 0.0,
bid_volume_3: 0.0,
bid_volume_4: 0.0,
bid_volume_5: 0.0,
ask_volume_2: 0.0,
ask_volume_3: 0.0,
ask_volume_4: 0.0,
ask_volume_5: 0.0,
last_volume: 0.0,
localtime: None,
pct_change: 0.0,
};
if dm.bid_volume2 != 0.0 || dm.ask_volume2 != 0.0 {
tick.bid_price_2 = adjust_price(dm.bid_price2);
tick.bid_price_3 = adjust_price(dm.bid_price3);
tick.bid_price_4 = adjust_price(dm.bid_price4);
tick.bid_price_5 = adjust_price(dm.bid_price5);
tick.ask_price_2 = adjust_price(dm.ask_price2);
tick.ask_price_3 = adjust_price(dm.ask_price3);
tick.ask_price_4 = adjust_price(dm.ask_price4);
tick.ask_price_5 = adjust_price(dm.ask_price5);
tick.bid_volume_2 = dm.bid_volume2;
tick.bid_volume_3 = dm.bid_volume3;
tick.bid_volume_4 = dm.bid_volume4;
tick.bid_volume_5 = dm.bid_volume5;
tick.ask_volume_2 = dm.ask_volume2;
tick.ask_volume_3 = dm.ask_volume3;
tick.ask_volume_4 = dm.ask_volume4;
tick.ask_volume_5 = dm.ask_volume5;
}
tick
}
pub fn contract_from(inf: &InstrumentField, gw: &str) -> ContractData {
ContractData {
gateway_name: gw.to_string(),
symbol: inf.instrument_id.clone(),
name: inf.instrument_name.clone(),
exchange: exchange_of(&inf.exchange_id).unwrap_or(Exchange::UNKNOWN),
asset: asset_from(&inf.product_class).unwrap_or(Asset::UNKNOWN),
instrument_type: instrument_type_from(&inf.product_class),
size: inf.volume_multiple,
price_tick: inf.price_tick,
max_limit_order_volume: Some(inf.max_limit_order_volume),
min_limit_order_volume: Some(inf.min_limit_order_volume),
max_market_order_volume: Some(inf.max_market_order_volume),
min_market_order_volume: Some(inf.min_market_order_volume),
delivery_year: Some(inf.delivery_year as i32),
delivery_month: Some(inf.delivery_month as i32),
product_id: Some(inf.product_id.clone()),
is_trading: Some(inf.is_trading == 1),
open_date: (!inf.open_date.is_empty()).then(|| inf.open_date.clone()),
up_stop_price: None,
down_stop_price: None,
pre_close: None,
float_volume: None,
total_volume: None,
instrument_status: None,
}
}
pub fn order_from(of: &OrderField, exchange: Exchange, gw: &str) -> OrderData {
let orderid = format!("{}_{}_{}", of.front_id, of.session_id, of.order_ref);
OrderData {
gateway_name: gw.to_string(),
symbol: of.instrument_id.clone(),
exchange,
orderid,
order_type: order_type_from(
&of.order_price_type,
&of.time_condition,
&of.volume_condition,
)
.unwrap_or(OrderType::UNKNOWN),
direction: direction_from(&of.direction),
offset: offset_from(&of.comb_offset_flag).unwrap_or(Offset::NONE),
price: adjust_price(of.limit_price),
volume: of.volume_total_original,
traded: of.volume_traded,
status: status_of(&of.order_status).unwrap_or(Status::UNKNOWN),
datetime: ctp_datetime(&of.insert_date, &of.insert_time, 0),
reference: String::new(),
name: String::new(),
status_msg: String::new(),
traded_price: 0.0,
}
}
pub fn trade_from(tf: &TradeField, orderid: &str, exchange: Exchange, gw: &str) -> TradeData {
TradeData {
gateway_name: gw.to_string(),
symbol: tf.instrument_id.clone(),
exchange,
orderid: orderid.to_string(),
tradeid: tf.trade_id.clone(),
direction: direction_from(&tf.direction),
offset: offset_from(&tf.offset_flag).unwrap_or(Offset::NONE),
price: tf.price,
volume: tf.volume,
datetime: ctp_datetime(&tf.trade_date, &tf.trade_time, 0),
commission: 0.0,
slippage: 0.0,
trade_pnl: 0.0,
name: String::new(),
traded_amount: 0.0,
}
}
pub fn account_from(ta: &TradingAccountField, gw: &str) -> AccountData {
AccountData {
gateway_name: gw.to_string(),
accountid: ta.account_id.clone(),
balance: ta.balance,
frozen: ta.frozen_margin + ta.frozen_cash + ta.frozen_commission,
}
}
pub fn accumulate_position(
row: &InvestorPositionField,
pos: &mut ek_data::PositionData,
exchange: Exchange,
contract_size: f64,
) {
if exchange == Exchange::SHFE || exchange == Exchange::INE {
if row.yd_position != 0.0 && row.today_position == 0.0 {
pos.yd_volume = row.position;
}
} else {
pos.yd_volume = row.position - row.today_position;
}
let cost = pos.price * pos.volume * contract_size;
pos.volume += row.position;
pos.pnl += row.position_profit;
if pos.volume != 0.0 && contract_size != 0.0 {
pos.price = (cost + row.position_cost) / (pos.volume * contract_size);
}
if pos.direction == Direction::LONG {
pos.frozen += row.short_frozen;
} else {
pos.frozen += row.long_frozen;
}
}
pub fn new_position_data(
symbol: &str,
exchange: Exchange,
direction: Option<Direction>,
gw: &str,
) -> Option<ek_data::PositionData> {
Some(ek_data::PositionData {
gateway_name: gw.to_string(),
symbol: symbol.to_string(),
exchange,
direction: direction?,
..ek_data::PositionData::default()
})
}
pub fn cancel_action_from(
orderid: &str,
symbol: &str,
exchange: Exchange,
broker: &str,
user: &str,
) -> Option<InputOrderAction> {
let mut parts = orderid.split('_');
let (front, session, ore) = (parts.next()?, parts.next()?, parts.next()?);
if parts.next().is_some() {
return None;
}
let (front_id, session_id): (i64, i64) = (front.parse().ok()?, session.parse().ok()?);
Some(InputOrderAction {
instrument_id: symbol.to_string(),
exchange_id: exchange.as_str().to_string(),
order_ref: ore.to_string(),
front_id,
session_id,
action_flag: "0".to_string(),
broker_id: broker.to_string(),
investor_id: user.to_string(),
})
}
pub fn instrument_id(raw: &str) -> Option<String> {
let s = raw.trim();
if s.is_empty() {
return None;
}
match s.split_once('.') {
None => Some(s.to_string()),
Some((head, rest)) => {
let rest = rest.trim();
if head.is_empty() || rest.is_empty() {
return None;
}
exchange_of(&head.trim().to_ascii_uppercase()).map(|_| rest.to_string())
}
}
}
pub fn position_direction(code: &str, position: f64) -> Option<Direction> {
match code {
PD_NET => match position.partial_cmp(&0.0)? {
std::cmp::Ordering::Greater => Some(Direction::LONG),
std::cmp::Ordering::Less => Some(Direction::SHORT),
std::cmp::Ordering::Equal => None,
},
PD_LONG => Some(Direction::LONG),
PD_SHORT => Some(Direction::SHORT),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contract_carries_open_date_from_wire() {
let inf = InstrumentField {
instrument_id: "rb2611".into(),
exchange_id: "SHFE".into(),
instrument_name: "螺纹钢2611".into(),
product_class: "1".into(),
volume_multiple: 10.0,
price_tick: 1.0,
max_limit_order_volume: 500.0,
min_limit_order_volume: 1.0,
max_market_order_volume: 500.0,
min_market_order_volume: 1.0,
delivery_year: 2026,
delivery_month: 11,
product_id: "rb".into(),
is_trading: 1,
open_date: "20260115".into(),
};
let c = contract_from(&inf, "ctp");
assert_eq!(c.open_date.as_deref(), Some("20260115"), "上市日必须透传");
}
#[test]
fn contract_open_date_empty_maps_to_none() {
let inf = InstrumentField {
instrument_id: "x".into(),
exchange_id: "SHFE".into(),
instrument_name: "x".into(),
product_class: "1".into(),
volume_multiple: 10.0,
price_tick: 1.0,
max_limit_order_volume: 1.0,
min_limit_order_volume: 1.0,
max_market_order_volume: 1.0,
min_market_order_volume: 1.0,
delivery_year: 2026,
delivery_month: 1,
product_id: "x".into(),
is_trading: 1,
open_date: String::new(),
};
let c = contract_from(&inf, "ctp");
assert_eq!(c.open_date, None);
}
#[test]
fn yyyymmdd_i32_accepts_real_dates_and_rejects_garbage() {
assert_eq!(yyyymmdd_i32("20261015"), Some(20261015));
assert_eq!(yyyymmdd_i32("00000000"), None);
assert_eq!(yyyymmdd_i32("2026101"), None);
assert_eq!(yyyymmdd_i32("202610150"), None);
assert_eq!(yyyymmdd_i32("2026101a"), None);
assert_eq!(yyyymmdd_i32(""), None);
}
#[test]
fn instrument_id_accepts_bare_and_en_symbol_and_rejects_garbage() {
assert_eq!(instrument_id("ag2612").as_deref(), Some("ag2612"));
assert_eq!(instrument_id(" cu2612 ").as_deref(), Some("cu2612"));
assert_eq!(instrument_id("SHFE.ag2612").as_deref(), Some("ag2612"));
assert_eq!(instrument_id("INE.sc2612").as_deref(), Some("sc2612"));
assert_eq!(instrument_id("CFFEX.IF2412").as_deref(), Some("IF2412"));
assert_eq!(instrument_id(""), None);
assert_eq!(instrument_id(" "), None);
assert_eq!(instrument_id("SHFE."), None);
assert_eq!(instrument_id(".ag2612"), None);
assert_eq!(instrument_id("XX.ag2612"), None);
assert_eq!(instrument_id("shfe.ag2612").as_deref(), Some("ag2612"));
assert_eq!(instrument_id("Shfe. ag2612 ").as_deref(), Some("ag2612"));
}
#[test]
fn offset_to_folds_close_today_by_exchange() {
for ex in [Exchange::SHFE, Exchange::INE] {
assert_eq!(offset_to(ex, Offset::CLOSETODAY), Some(OFEN_CLOSE_TODAY));
assert_eq!(
offset_to(ex, Offset::CLOSEYESTERDAY),
Some(OFEN_CLOSE_YESTERDAY)
);
assert_eq!(offset_to(ex, Offset::OPEN), Some(OFEN_OPEN));
assert_eq!(offset_to(ex, Offset::CLOSE), Some(OFEN_CLOSE));
}
for ex in [
Exchange::CFFEX,
Exchange::CZCE,
Exchange::DCE,
Exchange::GFEX,
] {
assert_eq!(
offset_to(ex, Offset::CLOSETODAY),
Some(OFEN_CLOSE),
"{ex:?}"
);
assert_eq!(
offset_to(ex, Offset::CLOSEYESTERDAY),
Some(OFEN_CLOSE),
"{ex:?}"
);
assert_eq!(offset_to(ex, Offset::OPEN), Some(OFEN_OPEN));
assert_eq!(offset_to(ex, Offset::CLOSE), Some(OFEN_CLOSE));
}
}
#[test]
fn status_of_covers_part_and_no_trade_not_queueing() {
assert_eq!(status_of("2"), Some(Status::PARTTRADED));
assert_eq!(status_of("4"), Some(Status::NOTTRADED));
assert_eq!(status_of("0"), Some(Status::ALLTRADED));
assert_eq!(status_of("1"), Some(Status::PARTTRADED));
assert_eq!(status_of("3"), Some(Status::NOTTRADED));
assert_eq!(status_of("5"), Some(Status::CANCELLED));
assert_eq!(status_of("a"), Some(Status::SUBMITTING));
assert_eq!(status_of("z"), None);
}
#[test]
fn position_direction_uses_sign_for_net_positions() {
assert_eq!(position_direction("1", 3.0), Some(Direction::LONG));
assert_eq!(position_direction("1", -2.0), Some(Direction::SHORT));
assert_eq!(position_direction("1", 0.0), None);
assert_eq!(position_direction("2", 5.0), Some(Direction::LONG));
assert_eq!(position_direction("3", 5.0), Some(Direction::SHORT));
assert_eq!(position_direction("9", 1.0), None);
}
#[test]
fn order_from_normalizes_market_order_sentinel_price() {
let of = OrderField {
instrument_id: "ag2612".into(),
exchange_id: "SHFE".into(),
order_ref: "1".into(),
front_id: 1,
session_id: 2,
order_status: "3".into(),
order_price_type: "1".into(),
time_condition: "3".into(),
volume_condition: "1".into(),
insert_date: "20261030".into(),
insert_time: "00:15:00".into(),
direction: "0".into(),
comb_offset_flag: "0".into(),
limit_price: MAX_FLOAT,
volume_total_original: 1.0,
volume_traded: 0.0,
..OrderField::default()
};
let order = order_from(&of, Exchange::SHFE, "ctp");
assert_eq!(order.price, 0.0);
assert_eq!(order.symbol, "ag2612");
assert_eq!(order.status, Status::NOTTRADED);
}
}