use crate::map::{self, Mapped};
use crate::wire::{
DepthMarketData, ErrorField, InputOrder, InputOrderAction, InstrumentCommissionRateField,
InstrumentField, InstrumentMarginRateField, InstrumentStatusField, InvestorPositionField,
OrderField, TradeField, TradingAccountField,
};
use ek_data::constant::{Direction, Offset, OrderType, Status};
use ek_data::log::{LEVEL_ERROR, LEVEL_INFO, LogData};
use ek_data::{
AccountData, CancelRequest, ContractData, OrderData, OrderRequest, PositionData, TickData,
TradeData,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use ek_data::log::LEVEL_WARNING;
pub type Contracts = Arc<Mutex<HashMap<String, ContractData>>>;
pub fn contracts_empty() -> Contracts {
Arc::new(Mutex::new(HashMap::new()))
}
pub fn log_info(gw: &str, msg: impl Into<String>) -> LogData {
LogData {
gateway_name: gw.to_string(),
msg: msg.into(),
level: LEVEL_INFO,
}
}
pub fn log_error_with(api_msg: &str, gw: &str, e: &ErrorField) -> LogData {
LogData {
gateway_name: gw.to_string(),
msg: map::error_text(api_msg, e),
level: LEVEL_ERROR,
}
}
pub fn log_error(gw: &str, msg: impl Into<String>) -> LogData {
LogData {
gateway_name: gw.to_string(),
msg: msg.into(),
level: LEVEL_ERROR,
}
}
pub fn log_warn(gw: &str, msg: impl Into<String>) -> LogData {
LogData {
gateway_name: gw.to_string(),
msg: msg.into(),
level: LEVEL_WARNING,
}
}
const INSTRUMENT_MAX_ATTEMPTS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryPhase {
Idle,
Instrument,
Rotating,
Halted,
}
impl QueryPhase {
#[must_use]
pub fn label(self) -> &'static str {
match self {
QueryPhase::Idle => "尚未开始查询",
QueryPhase::Instrument => "查询合约中",
QueryPhase::Rotating => "资金/持仓轮询中",
QueryPhase::Halted => "合约未到手,本轮查询已停止",
}
}
}
pub const CHAIN_STATE_PREFIX: &str = "[链] ";
const INSTRUMENT_FIRST_WAIT_MS: u64 = 1000;
const INSTRUMENT_RETRY_WAIT_MS: u64 = 5000;
const QUERY_MIN_INTERVAL_MS: u64 = 1000;
#[derive(Clone, Default)]
pub struct MdConfig {
pub addr: String,
pub user: String,
pub password: String,
pub broker: String,
}
impl std::fmt::Debug for MdConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MdConfig")
.field("addr", &self.addr)
.field("user", &self.user)
.field("password", &"****")
.field("broker", &self.broker)
.finish()
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum MdEvent {
FrontConnected,
FrontDisconnected(i64),
RspUserLogin {
error: Option<ErrorField>,
},
RspError(ErrorField),
RspSubMarketData {
error: Option<ErrorField>,
},
RtnDepthMarketData(DepthMarketData),
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum MdOut {
ReqConnect {
addr: String,
flow_path: String,
},
ReqLogin {
user: String,
password: String,
broker: String,
},
ReqSubscribe(Vec<String>),
Tick(TickData),
Log(LogData),
Status(crate::plugin::GatewayChange),
}
pub struct MdDriver {
pub gw: String,
cfg: MdConfig,
contracts: Contracts,
subscribed: Vec<String>,
login_status: bool,
connect_status: bool,
current_date: String,
disconnects: u32,
last_login_ok_ms: Option<i64>,
last_tick_ms: Option<i64>,
sub_rejects: u32,
last_error: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct MdStatusView {
pub handle_up: bool,
pub logged_in: bool,
pub disconnects: u32,
pub last_login_ok_ms: Option<i64>,
pub last_tick_ms: Option<i64>,
pub sub_rejects: u32,
pub last_error: Option<String>,
pub subscribed: usize,
}
impl MdDriver {
pub fn new(gw: &str, cfg: MdConfig, contracts: Contracts, current_date: String) -> Self {
Self {
gw: gw.to_string(),
cfg,
contracts,
subscribed: Vec::new(),
login_status: false,
connect_status: false,
current_date,
disconnects: 0,
last_login_ok_ms: None,
last_tick_ms: None,
sub_rejects: 0,
last_error: None,
}
}
pub fn update_date(&mut self, date: String) {
self.current_date = date;
}
pub fn subscribe(&mut self, symbol: &str) -> Vec<MdOut> {
if !self.subscribed.iter().any(|s| s == symbol) {
self.subscribed.push(symbol.to_string());
}
if self.login_status {
vec![MdOut::ReqSubscribe(vec![symbol.to_string()])]
} else {
Vec::new()
}
}
pub fn connect(&mut self) -> Vec<MdOut> {
if self.connect_status {
return Vec::new();
}
self.connect_status = true;
vec![MdOut::ReqConnect {
addr: self.cfg.addr.clone(),
flow_path: format!("{}\\Md", self.gw),
}]
}
pub fn reconfigure(&mut self, cfg: MdConfig) {
self.cfg = cfg;
self.connect_status = false;
self.login_status = false;
}
#[must_use]
pub fn status_view(&self) -> MdStatusView {
MdStatusView {
handle_up: self.connect_status,
logged_in: self.login_status,
disconnects: self.disconnects,
last_login_ok_ms: self.last_login_ok_ms,
last_tick_ms: self.last_tick_ms,
sub_rejects: self.sub_rejects,
last_error: self.last_error.clone(),
subscribed: self.subscribed.len(),
}
}
pub fn reset_session(&mut self) {
self.connect_status = false;
self.login_status = false;
}
pub fn step(&mut self, ev: MdEvent) -> Vec<MdOut> {
match ev {
MdEvent::FrontConnected => {
self.connect_status = true;
vec![
MdOut::Log(log_info(&self.gw, "行情服务器连接成功")),
MdOut::Status(crate::plugin::GatewayChange::MdConnected),
MdOut::ReqLogin {
user: self.cfg.user.clone(),
password: self.cfg.password.clone(),
broker: self.cfg.broker.clone(),
},
]
}
MdEvent::FrontDisconnected(reason) => {
self.login_status = false;
self.disconnects += 1;
self.last_error = Some(format!("行情服务器连接断开,原因{reason}"));
vec![
MdOut::Log(log_info(
&self.gw,
format!("行情服务器连接断开,原因{reason}"),
)),
MdOut::Status(crate::plugin::GatewayChange::MdDisconnected),
]
}
MdEvent::RspUserLogin { error } => match error {
None => {
self.login_status = true;
self.last_login_ok_ms = Some(crate::map::now_epoch_ms());
self.last_error = None;
let mut outs = vec![
MdOut::Log(log_info(&self.gw, "行情服务器登录成功")),
MdOut::Status(crate::plugin::GatewayChange::MdLoginOk),
];
if !self.subscribed.is_empty() {
outs.push(MdOut::ReqSubscribe(self.subscribed.clone()));
}
outs
}
Some(e) => {
self.last_error = Some(map::error_text("行情服务器登录失败", &e));
vec![
MdOut::Log(log_error_with("行情服务器登录失败", &self.gw, &e)),
MdOut::Status(crate::plugin::GatewayChange::MdLoginFailed),
]
}
},
MdEvent::RspError(e) => {
self.last_error = Some(map::error_text("行情接口报错", &e));
vec![MdOut::Log(log_error_with("行情接口报错", &self.gw, &e))]
}
MdEvent::RspSubMarketData { error } => match error {
Some(e) => {
self.sub_rejects += 1;
self.last_error = Some(map::error_text("行情订阅失败", &e));
vec![
MdOut::Log(log_error_with("行情订阅失败", &self.gw, &e)),
MdOut::Status(crate::plugin::GatewayChange::SubscribeRejected),
]
}
None => Vec::new(),
},
MdEvent::RtnDepthMarketData(dm) => {
self.last_tick_ms = Some(crate::map::now_epoch_ms());
let contract = {
let m = self.contracts.lock().unwrap();
m.get(&dm.instrument_id).cloned()
};
match map::tick_from(&dm, contract.as_ref(), &self.gw, &self.current_date) {
Mapped::Ok(t) => vec![MdOut::Tick(t)],
Mapped::Skip(_) => Vec::new(),
}
}
}
}
}
#[derive(Clone, Default)]
pub struct TdConfig {
pub addr: String,
pub user: String,
pub password: String,
pub broker: String,
pub auth_code: String,
pub app_id: String,
pub product_classes: map::ProductClasses,
}
impl std::fmt::Debug for TdConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TdConfig")
.field("addr", &self.addr)
.field("user", &self.user)
.field("password", &"****")
.field("broker", &self.broker)
.field("auth_code", &self.auth_code)
.field("app_id", &self.app_id)
.field("product_classes", &self.product_classes)
.finish()
}
}
#[derive(Debug, Clone)]
pub enum TdEvent {
FrontConnected,
FrontDisconnected(i64),
RspAuthenticate {
error: Option<ErrorField>,
},
RspUserLogin {
error: Option<ErrorField>,
front_id: i64,
session_id: i64,
},
RspSettlementInfoConfirm {
error: Option<ErrorField>,
},
RspQryInstrument {
last: bool,
row: Option<InstrumentField>,
error: Option<ErrorField>,
},
RspQryInvestorPosition {
last: bool,
row: Option<InvestorPositionField>,
error: Option<ErrorField>,
},
RspQryTradingAccount {
last: bool,
row: Option<TradingAccountField>,
error: Option<ErrorField>,
},
RspOrderInsert {
error: Option<ErrorField>,
req: Option<InputOrder>,
},
RspOrderAction {
error: Option<ErrorField>,
},
RtnOrder(OrderField),
RtnTrade(TradeField),
RspQryInstrumentMarginRate {
row: Option<InstrumentMarginRateField>,
error: Option<ErrorField>,
last: bool,
},
RspQryInstrumentCommissionRate {
row: Option<InstrumentCommissionRateField>,
error: Option<ErrorField>,
last: bool,
},
RtnInstrumentStatus(InstrumentStatusField),
}
#[derive(Debug, Clone)]
pub enum TdOut {
ReqConnect {
addr: String,
flow_path: String,
},
ReqAuthenticate {
user: String,
broker: String,
auth_code: String,
app_id: String,
},
ReqLogin {
user: String,
password: String,
broker: String,
},
ReqSettlementConfirm {
broker: String,
user: String,
},
ReqQryInstrument,
ReqQryAccount {
broker: String,
user: String,
},
ReqQryPosition {
broker: String,
user: String,
},
Insert(InputOrder),
Cancel(InputOrderAction),
Contract(ContractData),
Order(OrderData),
Trade(TradeData),
Position(PositionData),
PositionQueryDone(String),
ContractFinish(String),
Account(AccountData),
AccountQueryDone(String),
Log(LogData),
Status(crate::plugin::GatewayChange),
}
struct PositionAccum {
pos: PositionData,
}
pub struct TdDriver {
gw: String,
cfg: TdConfig,
contracts: Contracts,
pub reqid: i64,
order_ref: i64,
connect_status: bool,
login_status: bool,
auth_status: bool,
auth_failed: bool,
login_failed: bool,
front_id: i64,
session_id: i64,
positions: HashMap<String, PositionAccum>,
sysid_orderid: HashMap<String, String>,
next_pos: bool,
phase: QueryPhase,
instrument_attempts: u32,
instrument_warned: bool,
settlement_req_ms: Option<u64>,
last_query_ms: Option<u64>,
now_ms: Arc<dyn Fn() -> u64 + Send + Sync>,
status_seen: std::collections::HashSet<String>,
contract_kept: usize,
contract_filtered: usize,
contract_unmapped: usize,
contract_unmapped_codes: std::collections::BTreeSet<String>,
accepted: Vec<String>,
filtered_report_seen: std::collections::HashSet<String>,
disconnects: u32,
last_login_ok_ms: Option<i64>,
last_account_ms: Option<i64>,
last_position_ms: Option<i64>,
query_rejects: u32,
query_reject_streak: u32,
instrument_rejects: u32,
last_error: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct TdStatusView {
pub handle_up: bool,
pub logged_in: bool,
pub login_failed: bool,
pub auth_ok: bool,
pub auth_failed: bool,
pub front_id: i64,
pub session_id: i64,
pub disconnects: u32,
pub last_login_ok_ms: Option<i64>,
pub last_account_ms: Option<i64>,
pub last_position_ms: Option<i64>,
pub query_rejects: u32,
pub query_reject_streak: u32,
pub instrument_rejects: u32,
pub last_error: Option<String>,
}
impl TdDriver {
pub fn new(gw: &str, cfg: TdConfig, contracts: Contracts) -> Self {
let anchor = std::time::Instant::now();
Self::with_clock(
gw,
cfg,
contracts,
Arc::new(move || anchor.elapsed().as_millis() as u64),
)
}
pub fn with_clock(
gw: &str,
cfg: TdConfig,
contracts: Contracts,
now_ms: Arc<dyn Fn() -> u64 + Send + Sync>,
) -> Self {
let accepted = cfg.product_classes.normalize();
Self {
gw: gw.to_string(),
cfg,
contracts,
reqid: 0,
order_ref: 0,
connect_status: false,
login_status: false,
auth_status: false,
auth_failed: false,
login_failed: false,
front_id: 0,
session_id: 0,
positions: HashMap::new(),
sysid_orderid: HashMap::new(),
next_pos: false,
phase: QueryPhase::Idle,
instrument_attempts: 0,
instrument_warned: false,
settlement_req_ms: None,
last_query_ms: None,
now_ms,
status_seen: std::collections::HashSet::new(),
contract_kept: 0,
contract_filtered: 0,
contract_unmapped: 0,
contract_unmapped_codes: std::collections::BTreeSet::new(),
accepted,
filtered_report_seen: std::collections::HashSet::new(),
disconnects: 0,
last_login_ok_ms: None,
last_account_ms: None,
last_position_ms: None,
query_rejects: 0,
query_reject_streak: 0,
instrument_rejects: 0,
last_error: None,
}
}
pub fn local_orderid(&self, order_ref: &str) -> String {
format!("{}_{}_{}", self.front_id, self.session_id, order_ref)
}
pub fn auth_failed(&self) -> bool {
self.auth_failed
}
pub fn product_classes(&self) -> Vec<String> {
self.accepted.clone()
}
pub fn contract_counts(&self) -> (usize, usize, usize) {
(
self.contract_kept,
self.contract_filtered,
self.contract_unmapped,
)
}
pub fn contract_name(&self, symbol: &str) -> Option<String> {
let m = self.contracts.lock().unwrap();
m.get(symbol)
.map(|c| c.name.clone())
.filter(|n| !n.is_empty())
}
fn report_drop_log(&mut self, symbol: &str, exchange_id: &str, kind: &str) -> Vec<TdOut> {
if !self.filtered_report_seen.insert(symbol.to_string()) {
return Vec::new();
}
vec![TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: format!("{kind}回报交易所码未映射,丢弃:{symbol}(ExchangeID={exchange_id})"),
level: LEVEL_WARNING,
})]
}
pub fn login_status(&self) -> bool {
self.login_status
}
pub fn login_failed(&self) -> bool {
self.login_failed
}
pub fn positions_len(&self) -> usize {
self.positions.len()
}
pub fn front_id(&self) -> i64 {
self.front_id
}
pub fn connect_status(&self) -> bool {
self.connect_status
}
pub fn session_id(&self) -> i64 {
self.session_id
}
pub fn order_ref(&self) -> i64 {
self.order_ref
}
pub fn send_order(&mut self, req: &OrderRequest) -> SendOutcome {
let Some(offset_code) = map::offset_to(req.offset) else {
return SendOutcome::rejected(log_info(&self.gw, "请选择开平方向"));
};
let Some((price_type, time_cond, vol_cond)) = map::order_type_to(req.order_type) else {
return SendOutcome::rejected(log_info(
&self.gw,
format!("当前接口不支持该类型的委托{}", req.order_type.as_str()),
));
};
self.order_ref += 1;
let order_ref = format!("{}", self.order_ref);
let input = InputOrder {
instrument_id: req.symbol.clone(),
exchange_id: req.exchange.as_str().to_string(),
limit_price: req.price,
volume_total_original: req.volume as i64,
order_price_type: price_type.to_string(),
direction: map::direction_to(req.direction).unwrap_or("").to_string(),
comb_offset_flag: offset_code.to_string(),
order_ref: order_ref.clone(),
investor_id: self.cfg.user.clone(),
user_id: self.cfg.user.clone(),
broker_id: self.cfg.broker.clone(),
comb_hedge_flag: "1".to_string(),
contingent_condition: "1".to_string(),
force_close_reason: "0".to_string(),
is_auto_suspend: 0,
time_condition: time_cond.to_string(),
volume_condition: vol_cond.to_string(),
min_volume: 1,
};
let orderid = self.local_orderid(&order_ref);
let outs = vec![
TdOut::Insert(input),
TdOut::Order(req.create_order_data(&orderid, &self.gw)),
];
SendOutcome::placed(outs, format!("{}.{}", self.gw, orderid))
}
pub fn flow_path(&self) -> String {
format!("{}\\Td", self.gw)
}
pub fn connect(&mut self) -> Vec<TdOut> {
if self.connect_status {
return self.authenticate();
}
self.connect_status = true;
vec![TdOut::ReqConnect {
addr: self.cfg.addr.clone(),
flow_path: self.flow_path(),
}]
}
pub fn reconfigure(&mut self, cfg: TdConfig) {
self.cfg = cfg;
self.connect_status = false;
self.login_status = false;
self.auth_status = false;
self.auth_failed = false;
self.login_failed = false;
self.phase = QueryPhase::Idle;
self.instrument_attempts = 0;
self.instrument_warned = false;
self.settlement_req_ms = None;
self.last_query_ms = None;
self.next_pos = false;
self.contract_kept = 0;
self.contract_filtered = 0;
self.contract_unmapped = 0;
self.contract_unmapped_codes.clear();
self.accepted = self.cfg.product_classes.normalize();
self.filtered_report_seen.clear();
if !self.cfg.addr.is_empty() {
self.contracts.lock().unwrap().clear();
}
}
pub fn note_query_send_failure(&mut self, label: &str, code: i32) -> Vec<LogData> {
if code == 0 {
if self.query_reject_streak == 0 {
return Vec::new();
}
let n = self.query_reject_streak;
self.query_reject_streak = 0;
self.last_error = None;
return vec![log_info(
&self.gw,
format!("{label}查询发送恢复(此前连续被拒 {n} 次)"),
)];
}
self.query_rejects += 1;
let first_in_streak = self.query_reject_streak == 0;
self.query_reject_streak += 1;
self.last_error = Some(format!(
"{label}查询发送被拒,错误代码:{code}(连续 {} 次)",
self.query_reject_streak
));
if first_in_streak {
return vec![log_error(
&self.gw,
format!(
"{label}查询发送被拒,错误代码:{code}(连续被拒 {} 次;与回报侧被拒同计一条连续串)",
self.query_reject_streak
),
)];
}
Vec::new()
}
pub fn note_instrument_send_failure(&mut self, code: i32) -> Vec<LogData> {
let logs = self.note_query_send_failure("合约", code);
if code != 0 {
self.instrument_rejects += 1;
}
logs
}
#[must_use]
pub fn status_view(&self) -> TdStatusView {
TdStatusView {
handle_up: self.connect_status,
logged_in: self.login_status,
login_failed: self.login_failed,
auth_ok: self.auth_status,
auth_failed: self.auth_failed,
front_id: self.front_id,
session_id: self.session_id,
disconnects: self.disconnects,
last_login_ok_ms: self.last_login_ok_ms,
last_account_ms: self.last_account_ms,
last_position_ms: self.last_position_ms,
query_rejects: self.query_rejects,
query_reject_streak: self.query_reject_streak,
instrument_rejects: self.instrument_rejects,
last_error: self.last_error.clone(),
}
}
pub fn reset_session(&mut self) {
self.connect_status = false;
self.login_status = false;
self.auth_status = false;
self.phase = QueryPhase::Idle;
self.instrument_attempts = 0;
self.instrument_warned = false;
self.settlement_req_ms = None;
self.last_query_ms = None;
self.next_pos = false;
}
pub fn authenticate(&mut self) -> Vec<TdOut> {
if self.auth_failed {
return Vec::new();
}
vec![TdOut::ReqAuthenticate {
user: self.cfg.user.clone(),
broker: self.cfg.broker.clone(),
auth_code: self.cfg.auth_code.clone(),
app_id: self.cfg.app_id.clone(),
}]
}
pub fn login(&mut self) -> Vec<TdOut> {
if self.login_failed {
return Vec::new();
}
vec![TdOut::ReqLogin {
user: self.cfg.user.clone(),
password: self.cfg.password.clone(),
broker: self.cfg.broker.clone(),
}]
}
pub fn cancel_order(&mut self, req: &CancelRequest) -> Vec<TdOut> {
match map::cancel_action_from(
&req.orderid,
&req.symbol,
req.exchange,
&self.cfg.broker,
&self.cfg.user,
) {
Some(action) => vec![TdOut::Cancel(action)],
None => vec![TdOut::Log(log_info(
&self.gw,
format!("撤单请求委托号异常:{}", req.orderid),
))],
}
}
fn remember_sysid(&mut self, sysid: &str, orderid: &str) {
self.sysid_orderid
.insert(sysid.to_string(), orderid.to_string());
}
pub fn on_timer(&mut self) -> Vec<TdOut> {
if matches!(self.phase, QueryPhase::Idle | QueryPhase::Halted) {
return Vec::new();
}
let now = (self.now_ms)();
let outs = if self.phase == QueryPhase::Instrument {
self.instrument_beat(now)
} else {
self.rotation_beat(now)
};
if outs.iter().any(|o| !matches!(o, TdOut::Log(_))) {
self.last_query_ms = Some(now);
}
outs
}
fn first_instrument_due(&self, now: u64) -> bool {
match self.settlement_req_ms {
None => true,
Some(t) => now.saturating_sub(t) >= INSTRUMENT_FIRST_WAIT_MS,
}
}
fn since_last_reached(&self, now: u64, ms: u64) -> bool {
match self.last_query_ms {
None => true,
Some(last) => now.saturating_sub(last) >= ms,
}
}
fn instrument_beat(&mut self, now: u64) -> Vec<TdOut> {
if self.instrument_attempts == 0 {
if !self.first_instrument_due(now) {
return Vec::new();
}
return self.ask_instrument();
}
if self.instrument_attempts < INSTRUMENT_MAX_ATTEMPTS {
if !self.since_last_reached(now, INSTRUMENT_RETRY_WAIT_MS) {
return Vec::new();
}
return self.ask_instrument();
}
if !self.since_last_reached(now, INSTRUMENT_RETRY_WAIT_MS) {
return Vec::new();
}
self.phase = QueryPhase::Halted;
self.warn_no_contract()
}
fn rotation_beat(&mut self, now: u64) -> Vec<TdOut> {
if !self.since_last_reached(now, QUERY_MIN_INTERVAL_MS) {
return Vec::new();
}
self.rotate_account_position()
}
fn ask_instrument(&mut self) -> Vec<TdOut> {
self.instrument_attempts += 1;
let n = self.instrument_attempts;
vec![
TdOut::Log(log_info(
&self.gw,
format!("{CHAIN_STATE_PREFIX}查询合约 第{n}次"),
)),
TdOut::ReqQryInstrument,
]
}
fn warn_no_contract(&mut self) -> Vec<TdOut> {
if self.instrument_warned {
return Vec::new();
}
self.instrument_warned = true;
let msg = format!(
"{CHAIN_STATE_PREFIX}合约 {} 次未获合约信息:本轮查询已停止(不再问资金与持仓)",
INSTRUMENT_MAX_ATTEMPTS
);
self.last_error = Some(msg.clone());
vec![
TdOut::Log(log_warn(&self.gw, msg)),
TdOut::Status(crate::plugin::GatewayChange::ContractQueryStopped),
]
}
fn rotate_account_position(&mut self) -> Vec<TdOut> {
let out = if self.next_pos {
TdOut::ReqQryPosition {
broker: self.cfg.broker.clone(),
user: self.cfg.user.clone(),
}
} else {
TdOut::ReqQryAccount {
broker: self.cfg.broker.clone(),
user: self.cfg.user.clone(),
}
};
self.next_pos = !self.next_pos;
vec![out]
}
#[must_use]
pub fn instrument_retry(&self) -> (u32, bool, bool) {
(
self.instrument_attempts,
!matches!(self.phase, QueryPhase::Idle | QueryPhase::Instrument),
self.instrument_warned,
)
}
#[must_use]
pub fn query_phase(&self) -> QueryPhase {
self.phase
}
pub fn step(&mut self, ev: TdEvent) -> Vec<TdOut> {
match ev {
TdEvent::FrontConnected => {
self.connect_status = true;
let mut outs = vec![
TdOut::Log(log_info(&self.gw, "交易服务器连接成功")),
TdOut::Status(crate::plugin::GatewayChange::TdConnected),
];
if self.cfg.auth_code.is_empty() {
outs.extend(self.login());
} else {
outs.extend(self.authenticate());
}
outs
}
TdEvent::FrontDisconnected(reason) => {
self.login_status = false;
self.phase = QueryPhase::Idle;
self.disconnects += 1;
self.last_error = Some(format!("交易服务器连接断开,原因{reason}"));
vec![
TdOut::Log(log_info(
&self.gw,
format!("交易服务器连接断开,原因{reason}"),
)),
TdOut::Status(crate::plugin::GatewayChange::TdDisconnected),
]
}
TdEvent::RspAuthenticate { error } => match error {
None => {
self.auth_status = true;
vec![
TdOut::Log(log_info(&self.gw, "交易服务器授权验证成功")),
TdOut::Status(crate::plugin::GatewayChange::TdAuthOk),
TdOut::ReqLogin {
user: self.cfg.user.clone(),
password: self.cfg.password.clone(),
broker: self.cfg.broker.clone(),
},
]
}
Some(e) => {
self.last_error = Some(map::error_text("交易服务器授权验证失败", &e));
let outs = vec![
TdOut::Log(log_error_with("交易服务器授权验证失败", &self.gw, &e)),
TdOut::Status(crate::plugin::GatewayChange::TdAuthFailed),
];
if e.error_id == 63 {
self.auth_failed = true;
}
outs
}
},
TdEvent::RspUserLogin {
error,
front_id,
session_id,
} => match error {
None => {
self.front_id = front_id;
self.session_id = session_id;
self.login_status = true;
self.login_failed = false;
self.last_login_ok_ms = Some(crate::map::now_epoch_ms());
self.last_error = None;
self.settlement_req_ms = Some((self.now_ms)());
vec![
TdOut::Log(log_info(&self.gw, "交易服务器登录成功")),
TdOut::Status(crate::plugin::GatewayChange::TdLoginOk),
TdOut::ReqSettlementConfirm {
broker: self.cfg.broker.clone(),
user: self.cfg.user.clone(),
},
]
}
Some(e) => {
self.login_failed = true;
self.last_error = Some(map::error_text("交易服务器登录失败", &e));
vec![
TdOut::Log(log_error_with("交易服务器登录失败", &self.gw, &e)),
TdOut::Status(crate::plugin::GatewayChange::TdLoginFailed),
]
}
},
TdEvent::RspSettlementInfoConfirm { error: _ } => {
self.contract_kept = 0;
self.contract_filtered = 0;
self.contract_unmapped = 0;
self.contract_unmapped_codes.clear();
self.filtered_report_seen.clear();
self.phase = QueryPhase::Instrument;
self.instrument_attempts = 0;
self.instrument_warned = false;
self.next_pos = false;
let now = (self.now_ms)();
let mut outs = vec![
TdOut::Log(log_info(&self.gw, "结算信息确认成功")),
TdOut::Status(crate::plugin::GatewayChange::SettlementConfirmed),
];
if self.first_instrument_due(now) {
outs.extend(self.ask_instrument());
self.last_query_ms = Some(now);
}
outs
}
TdEvent::RspQryInstrument { last, row, error } => {
let mut outs = Vec::new();
if let Some(e) = &error {
self.query_rejects += 1;
self.query_reject_streak += 1;
self.instrument_rejects += 1;
self.last_error = Some(map::error_text("合约查询被拒", e));
outs.push(TdOut::Status(crate::plugin::GatewayChange::QueryRejected));
} else if row.is_some() {
self.query_reject_streak = 0;
self.last_error = None;
}
if let Some(inf) = row {
if matches!(self.phase, QueryPhase::Instrument | QueryPhase::Halted) {
self.phase = QueryPhase::Rotating;
outs.push(TdOut::Status(crate::plugin::GatewayChange::ContractArrived));
}
if !self.accepted.iter().any(|c| c == &inf.product_class) {
self.contract_filtered += 1;
} else {
match map::contract_from(&inf, &self.gw) {
Mapped::Ok(c) => {
self.contracts
.lock()
.unwrap()
.insert(c.symbol.clone(), c.clone());
self.contract_kept += 1;
outs.push(TdOut::Contract(c.clone()));
}
Mapped::Skip(_) => {
self.contract_unmapped += 1;
self.contract_unmapped_codes
.insert(inf.product_class.clone());
}
}
}
}
if last {
if let Some(e) = &error {
outs.push(TdOut::Log(log_error_with("合约查询被拒", &self.gw, e)));
} else {
let unmapped_codes = if self.contract_unmapped_codes.is_empty() {
String::new()
} else {
format!(
"(码:{})",
self.contract_unmapped_codes
.iter()
.cloned()
.collect::<Vec<_>>()
.join(",")
)
};
outs.push(TdOut::Log(log_info(
&self.gw,
format!(
"合约信息查询成功:收录 {} / 按勾选丢弃 {} / 未映射 {}{unmapped_codes}(收录产品类:{})",
self.contract_kept,
self.contract_filtered,
self.contract_unmapped,
self.accepted.join(" ")
),
)));
outs.push(TdOut::ContractFinish(self.gw.clone()));
}
}
outs
}
TdEvent::RspQryInvestorPosition { last, row, error } => {
let mut outs: Vec<TdOut> = Vec::new();
if let Some(e) = &error {
self.query_rejects += 1;
self.query_reject_streak += 1;
self.last_error = Some(map::error_text("持仓查询被拒", e));
outs.push(TdOut::Status(crate::plugin::GatewayChange::QueryRejected));
} else {
self.query_reject_streak = 0;
self.last_error = None;
if row.is_some() {
self.last_position_ms = Some(crate::map::now_epoch_ms());
}
}
let Some(row) = row else { return outs };
let mut tail = Vec::new();
{
let exchange = map::exchange_of(&row.exchange_id);
let size = exchange
.as_ref()
.map(|_| {
self.contracts
.lock()
.unwrap()
.get(&row.instrument_id)
.map(|c| c.size)
.unwrap_or(0.0)
})
.unwrap_or(0.0);
match exchange {
Some(exchange) => {
let key = format!("{}:{}", row.instrument_id, row.posi_direction);
let direction = map::direction_from(&row.posi_direction);
if !self.positions.contains_key(&key) {
let pos = map::new_position_data(
&row.instrument_id,
exchange,
direction,
&self.gw,
)
.unwrap_or_default();
self.positions.insert(key.clone(), PositionAccum { pos });
}
let entry = self.positions.get_mut(&key).expect("just inserted");
map::accumulate_position(&row, &mut entry.pos, exchange, size);
}
None => {
tail.extend(self.report_drop_log(
&row.instrument_id,
&row.exchange_id,
"持仓",
));
}
}
if last {
let done = self
.positions
.values()
.map(|p| p.pos.clone())
.collect::<Vec<_>>();
for pos in done {
tail.push(TdOut::Position(pos));
}
tail.push(TdOut::PositionQueryDone(self.gw.clone()));
self.positions.clear();
}
}
outs.extend(tail);
outs
}
TdEvent::RspQryTradingAccount { last, row, error } => {
let mut pre: Vec<TdOut> = Vec::new();
if let Some(e) = &error {
self.query_rejects += 1;
self.query_reject_streak += 1;
self.last_error = Some(map::error_text("资金查询被拒", e));
pre.push(TdOut::Status(crate::plugin::GatewayChange::QueryRejected));
} else {
self.query_reject_streak = 0;
self.last_error = None;
if row.is_some() {
self.last_account_ms = Some(crate::map::now_epoch_ms());
}
}
let mut outs = match row {
Some(ta) => {
let mut outs = vec![TdOut::Account(map::account_from(&ta, &self.gw))];
if self.phase != QueryPhase::Rotating {
let msg = format!(
"{CHAIN_STATE_PREFIX}资金回报到手但当前阶段是「{}」:轮询不自启",
self.phase.label()
);
outs.push(TdOut::Log(log_warn(&self.gw, msg)));
}
outs
}
None => Vec::new(),
};
outs.splice(0..0, pre);
if last && error.is_none() {
outs.push(TdOut::AccountQueryDone(self.gw.clone()));
}
outs
}
TdEvent::RspOrderInsert { error, req } => match (error, req) {
(Some(e), Some(input)) => {
let mut outs = vec![TdOut::Log(log_error_with("交易委托失败", &self.gw, &e))];
let contract = {
let m = self.contracts.lock().unwrap();
m.get(&input.instrument_id).cloned()
};
if let Some(contract) = contract {
let orderid = self.local_orderid(&input.order_ref);
let direction = map::direction_from(&input.direction);
let offset =
map::offset_from(&input.comb_offset_flag).unwrap_or(Offset::NONE);
outs.push(TdOut::Order(OrderData {
gateway_name: self.gw.clone(),
symbol: contract.symbol.clone(),
name: contract.name.clone(),
exchange: contract.exchange,
orderid,
order_type: OrderType::LIMIT,
direction: Some(direction.unwrap_or(Direction::NET)),
offset,
price: input.limit_price,
volume: input.volume_total_original as f64,
status: Status::REJECTED,
..OrderData::default()
}));
}
outs
}
_ => Vec::new(),
},
TdEvent::RspOrderAction { error } => match error {
Some(e) => vec![TdOut::Log(log_error_with("交易撤单失败", &self.gw, &e))],
None => Vec::new(),
},
TdEvent::RtnOrder(of) => {
let Some(exchange) = map::exchange_of(&of.exchange_id) else {
return self.report_drop_log(&of.instrument_id, &of.exchange_id, "委托");
};
match map::order_from(&of, exchange, &self.gw) {
Mapped::Ok(order) => {
let orderid = order.orderid.clone();
let outs = vec![TdOut::Order(order)];
self.remember_sysid(&of.order_sys_id, &orderid);
outs
}
Mapped::Skip(reason) => vec![TdOut::Log(log_info(&self.gw, reason))],
}
}
TdEvent::RtnTrade(tf) => {
let Some(exchange) = map::exchange_of(&tf.exchange_id) else {
return self.report_drop_log(&tf.instrument_id, &tf.exchange_id, "成交");
};
match self.sysid_orderid.get(&tf.order_sys_id) {
Some(orderid) => match map::trade_from(&tf, orderid, exchange, &self.gw) {
Mapped::Ok(trade) => vec![TdOut::Trade(trade)],
Mapped::Skip(reason) => vec![TdOut::Log(log_info(&self.gw, reason))],
},
None => vec![TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: format!("成交回报缺少在册委托:{}", tf.order_sys_id),
level: LEVEL_ERROR,
})],
}
}
TdEvent::RspQryInstrumentMarginRate { row, error, last } => {
let mut outs = Vec::new();
if let Some(e) = error {
outs.push(TdOut::Log(log_error_with("保证金率查询失败", &self.gw, &e)));
}
if let Some(r) = row {
if last {
outs.push(TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: format!(
"保证金率(末包):{} {} 档 long {}/{} short {}/{} 相对交易所={}",
r.instrument_id,
r.hedge_flag,
r.long_margin_ratio_by_money,
r.long_margin_ratio_by_volume,
r.short_margin_ratio_by_money,
r.short_margin_ratio_by_volume,
r.is_relative,
),
level: LEVEL_INFO,
}));
}
} else if last {
outs.push(TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: "保证金率查询:空结果(末包)".to_string(),
level: LEVEL_INFO,
}));
}
outs
}
TdEvent::RspQryInstrumentCommissionRate { row, error, last } => {
let mut outs = Vec::new();
if let Some(e) = error {
outs.push(TdOut::Log(log_error_with("手续费率查询失败", &self.gw, &e)));
}
if let Some(r) = row {
if last {
outs.push(TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: format!(
"手续费率(末包):{} 开 {}/{} 平 {}/{} 平今 {}/{}",
r.instrument_id,
r.open_ratio_by_money,
r.open_ratio_by_volume,
r.close_ratio_by_money,
r.close_ratio_by_volume,
r.close_today_ratio_by_money,
r.close_today_ratio_by_volume,
),
level: LEVEL_INFO,
}));
}
} else if last {
outs.push(TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: "手续费率查询:空结果(末包)".to_string(),
level: LEVEL_INFO,
}));
}
outs
}
TdEvent::RtnInstrumentStatus(s) => {
let key = format!(
"{}|{}|{}|{}",
s.exchange_id, s.instrument_id, s.instrument_status, s.trading_segment_sn
);
if !self.status_seen.insert(key) {
return Vec::new();
}
vec![TdOut::Log(LogData {
gateway_name: self.gw.clone(),
msg: format!(
"交易状态:{}/{} status={} 进入 @{} (原因 {})",
s.exchange_id,
s.instrument_id,
s.instrument_status,
s.enter_time,
s.enter_reason
),
level: LEVEL_INFO,
})]
}
}
}
}
pub struct SendOutcome {
pub en_orderid: String,
pub error: Option<LogData>,
pub outs: Vec<TdOut>,
}
impl SendOutcome {
fn rejected(log: LogData) -> Self {
Self {
en_orderid: String::new(),
error: Some(log),
outs: Vec::new(),
}
}
fn placed(outs: Vec<TdOut>, en_orderid: String) -> Self {
Self {
en_orderid,
error: None,
outs,
}
}
}