use crate::driver::{self, MdDriver, MdEvent, MdOut, TdDriver, TdEvent, TdOut};
use crate::wire::InputOrder;
use ek_core::context::PluginContext;
use ek_core::envelope::{Envelope, ReplyErr, e_req_type};
use ek_core::plugin::Plugin;
use ek_core::service::ServiceDesc;
use ek_data::cancel_request::CancelRequest;
use ek_data::log::LogData;
#[cfg(feature = "native")]
use ek_data::log::LEVEL_ERROR;
use ek_data::order_request::OrderRequest;
use ek_event::Event;
use std::borrow::Cow;
use std::sync::{Arc, Mutex, OnceLock};
const W_TICK: &str = "eTick.";
const W_ORDER: &str = "eOrder.";
const W_TRADE: &str = "eTrade.";
const W_POSITION: &str = "ePosition.";
const W_ACCOUNT: &str = "eAccount.";
const W_CONTRACT: &str = "eContract.";
const W_POS_DONE: &str = "ePositionDone.";
use ek_data::event::EVENT_LOG as W_LOG;
pub use ek_event::EVENT_TIMER as ROUTE_TIMER;
#[derive(Debug, Clone)]
pub struct SubscribeReq {
pub symbols: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubscribeReply {
pub ok: bool,
pub msg: String,
}
#[derive(Debug, Clone)]
pub struct SendOrderReq {
pub req: OrderRequest,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SendOrderReply {
pub ok: bool,
pub en_orderid: String,
pub msg: String,
}
#[derive(Debug, Clone)]
pub struct CancelOrderReq {
pub req: CancelRequest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancelOrderReply {
pub ok: bool,
pub msg: String,
}
#[derive(Debug, Clone)]
pub struct QueryRateReq {
pub kind: String,
pub instrument_id: String,
pub hedge_flag: String,
pub exchange_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryRateReply {
pub ok: bool,
pub msg: String,
}
#[derive(Debug, Clone, Default)]
pub struct ConfigureReq {
pub broker_id: String,
pub td_addr: String,
pub td_user: String,
pub td_password: String,
pub td_auth_code: String,
pub td_app_id: String,
pub md_addr: String,
pub md_user: String,
pub md_password: String,
pub product_classes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigureReply {
pub ok: bool,
pub msg: String,
pub product_classes: Vec<String>,
}
pub trait CtpTransport: Send + Sync {
fn md_connect(&self, addr: &str, flow_path: &str);
fn md_login(&self, user: &str, password: &str, broker: &str);
fn md_subscribe(&self, symbols: &[String]);
fn td_connect(&self, addr: &str, flow_path: &str);
fn td_auth(&self, user: &str, broker: &str, auth_code: &str, app_id: &str);
fn td_login(&self, user: &str, password: &str, broker: &str);
fn td_settlement_confirm(&self, broker: &str, user: &str);
fn td_qry_instrument(&self);
fn td_qry_account(&self, broker: &str, user: &str);
fn td_qry_position(&self, broker: &str, user: &str);
fn td_insert(&self, order: &InputOrder) -> i32;
fn td_cancel(&self, action: &crate::wire::InputOrderAction) -> i32;
fn td_qry_margin_rate(&self, query: &crate::wire::QryInstrumentMarginRate) -> i32;
fn td_qry_commission_rate(&self, query: &crate::wire::QryInstrumentCommissionRate) -> i32;
fn shutdown(&self) {}
fn td_release(&self) {}
fn md_release(&self) {}
}
#[derive(Debug, Clone, Default)]
pub struct CtpConfig {
pub gateway_name: String,
pub broker_id: String,
pub td_addr: String,
pub td_user: String,
pub td_password: String,
pub td_auth_code: String,
pub td_app_id: String,
pub md_addr: String,
pub md_user: String,
pub md_password: String,
pub product_classes: crate::map::ProductClasses,
}
impl CtpConfig {
pub fn gateway_name_or_default(&self) -> String {
if self.gateway_name.is_empty() {
"ctp".to_string()
} else {
self.gateway_name.clone()
}
}
}
pub struct CtpPlugin {
gw: String,
cfg: Mutex<CtpConfig>,
broker: Mutex<String>,
user: Mutex<String>,
md: Mutex<MdDriver>,
td: Mutex<TdDriver>,
timer_count: Mutex<u32>,
trans: Option<Box<dyn CtpTransport>>,
ctx: OnceLock<PluginContext>,
trans_warned: Mutex<bool>,
}
impl CtpPlugin {
pub fn new(cfg: CtpConfig) -> Self {
let gw = cfg.gateway_name_or_default();
Self::with_parts(cfg, gw, None)
}
pub fn with_transport(cfg: CtpConfig, trans: Box<dyn CtpTransport>) -> Self {
let gw = cfg.gateway_name_or_default();
Self::with_parts(cfg, gw, Some(trans))
}
fn with_parts(cfg: CtpConfig, gw: String, trans: Option<Box<dyn CtpTransport>>) -> Self {
let contracts = driver::contracts_empty();
let cfg_broker = cfg.broker_id.clone();
let cfg_user = cfg.td_user.clone();
let md_cfg = driver::MdConfig {
addr: cfg.md_addr.clone(),
user: cfg.md_user.clone(),
password: cfg.md_password.clone(),
broker: cfg.broker_id.clone(),
};
let td_cfg = driver::TdConfig {
addr: cfg.td_addr.clone(),
user: cfg.td_user.clone(),
password: cfg.td_password.clone(),
broker: cfg.broker_id.clone(),
auth_code: cfg.td_auth_code.clone(),
app_id: cfg.td_app_id.clone(),
product_classes: cfg.product_classes.clone(),
};
let md = Mutex::new(MdDriver::new(
&gw,
md_cfg,
contracts.clone(),
crate::map::shanghai_now_date(),
));
let td = Mutex::new(TdDriver::new(&gw, td_cfg, contracts.clone()));
Self {
gw,
cfg: Mutex::new(cfg),
broker: Mutex::new(cfg_broker),
user: Mutex::new(cfg_user),
md,
td,
timer_count: Mutex::new(0),
trans,
ctx: OnceLock::new(),
trans_warned: Mutex::new(false),
}
}
#[cfg(feature = "native")]
pub fn with_native(cfg: CtpConfig, flow_root: impl Into<std::path::PathBuf>) -> Arc<Self> {
let (trans, ingress) = crate::native::transport::native_parts(flow_root);
let plugin = Arc::new(Self::with_transport(cfg, trans));
crate::native::transport::attach_plugin(&ingress, &plugin);
plugin
}
pub fn connect(&self) {
let (td_empty, md_empty) = {
let c = self.cfg.lock().unwrap();
(c.td_addr.is_empty(), c.md_addr.is_empty())
};
if td_empty && md_empty {
self.put_log(LogData {
gateway_name: self.gw.clone(),
msg: "未配置前置地址,跳过连接(等待 configure)".to_string(),
level: 20,
});
return;
}
let td_outs = if td_empty {
Vec::new()
} else {
self.td.lock().unwrap().connect()
};
let md_outs = if md_empty {
Vec::new()
} else {
self.md.lock().unwrap().connect()
};
for o in td_outs {
self.emit_td(o);
}
for o in md_outs {
self.emit_md(o);
}
}
#[cfg(feature = "native")]
pub(crate) fn transport_log(&self, msg: String) {
self.put_log(LogData {
gateway_name: self.gw.clone(),
msg,
level: LEVEL_ERROR,
});
}
pub fn on_md_event(&self, ev: MdEvent) {
let outs = self.md.lock().unwrap().step(ev);
for o in outs {
self.emit_md(o);
}
}
pub fn on_td_event(&self, ev: TdEvent) {
let outs = self.td.lock().unwrap().step(ev);
for o in outs {
self.emit_td(o);
}
}
pub fn on_timer(&self) {
let outs = self.td.lock().unwrap().on_timer_tick();
for o in outs {
self.emit_td(o);
}
let gate_pass = {
let mut c = self.timer_count.lock().unwrap();
*c += 1;
if *c < 2 {
false
} else {
*c = 0;
true
}
};
if !gate_pass {
return;
}
let outs = self.td.lock().unwrap().on_timer();
for o in outs {
self.emit_td(o);
}
let date = crate::map::shanghai_now_date();
self.md.lock().unwrap().update_date(date);
}
fn emit_md(&self, o: MdOut) {
match o {
MdOut::Log(l) => self.put_log(l),
MdOut::Tick(t) => self.put_data(W_TICK, t),
MdOut::ReqConnect { addr, flow_path } => {
if let Some(t) = &self.trans {
t.md_connect(&addr, &flow_path);
} else {
self.warn_no_transport();
}
}
MdOut::ReqLogin {
user,
password,
broker,
} => {
if let Some(t) = &self.trans {
t.md_login(&user, &password, &broker);
} else {
self.warn_no_transport();
}
}
MdOut::ReqSubscribe(symbols) => {
if let Some(t) = &self.trans {
t.md_subscribe(&symbols);
} else {
self.warn_no_transport();
}
}
}
}
fn emit_td(&self, o: TdOut) {
match o {
TdOut::Log(l) => self.put_log(l),
TdOut::Contract(c) => self.put_data(W_CONTRACT, c),
TdOut::Order(o) => self.put_data(W_ORDER, o),
TdOut::Trade(t) => self.put_data(W_TRADE, t),
TdOut::Position(p) => self.put_data(W_POSITION, p),
TdOut::Account(a) => self.put_data(W_ACCOUNT, a),
TdOut::PositionQueryDone(gw) => {
if let Some(ctx) = self.ctx.get() {
let _ = ctx.put(Event::with_data(W_POS_DONE, Arc::new(gw)));
}
}
TdOut::ContractFinish(gw) => {
if let Some(ctx) = self.ctx.get() {
let _ = ctx.put(Event::with_data("eContractFinish", Arc::new(gw)));
}
}
TdOut::ReqConnect { addr, flow_path } => {
if let Some(t) = &self.trans {
t.td_connect(&addr, &flow_path);
} else {
self.warn_no_transport();
}
}
TdOut::ReqAuthenticate {
user,
broker,
auth_code,
app_id,
} => {
if let Some(t) = &self.trans {
t.td_auth(&user, &broker, &auth_code, &app_id);
} else {
self.warn_no_transport();
}
}
TdOut::ReqLogin {
user,
password,
broker,
} => {
if let Some(t) = &self.trans {
t.td_login(&user, &password, &broker);
} else {
self.warn_no_transport();
}
}
TdOut::ReqSettlementConfirm { broker, user } => {
if let Some(t) = &self.trans {
t.td_settlement_confirm(&broker, &user);
} else {
self.warn_no_transport();
}
}
TdOut::ReqQryInstrument => {
if let Some(t) = &self.trans {
t.td_qry_instrument();
} else {
self.warn_no_transport();
}
}
TdOut::ReqQryAccount { broker, user } => {
if let Some(t) = &self.trans {
t.td_qry_account(&broker, &user);
} else {
self.warn_no_transport();
}
}
TdOut::ReqQryPosition { broker, user } => {
if let Some(t) = &self.trans {
t.td_qry_position(&broker, &user);
} else {
self.warn_no_transport();
}
}
TdOut::Insert(o) => {
if let Some(t) = &self.trans {
t.td_insert(&o);
} else {
self.warn_no_transport();
}
}
TdOut::Cancel(a) => {
if let Some(t) = &self.trans {
t.td_cancel(&a);
} else {
self.warn_no_transport();
}
}
}
}
fn put_log(&self, l: LogData) {
if let Some(ctx) = self.ctx.get() {
let _ = ctx.put(Event::with_data(W_LOG, Arc::new(l)));
}
}
fn put_data<T: Send + Sync + 'static>(&self, prefix: &str, payload: T) {
if let Some(ctx) = self.ctx.get() {
let _ = ctx.put(Event::with_data(prefix, Arc::new(payload)));
}
}
fn warn_no_transport(&self) {
let mut warned = self.trans_warned.lock().unwrap();
if !*warned {
*warned = true;
self.put_log(LogData {
gateway_name: self.gw.clone(),
msg: "传输层未接入(默认构建):连接/查询/发送动作已被吸收".to_string(),
level: 20,
});
}
}
fn on_request(&self, event: &Event) {
let Some(env) = event.data.downcast_ref::<Envelope>() else {
return;
};
macro_rules! arm {
($ty:ty, $res:ty, $body:expr) => {
if let Some(req) = env.payload.downcast_ref::<$ty>() {
let res: $res = $body(req);
let _ = env.reply.send(Box::new(Ok::<$res, ReplyErr>(res)));
return;
}
};
}
arm!(SubscribeReq, SubscribeReply, |r: &SubscribeReq| self
.do_subscribe(r));
arm!(SendOrderReq, SendOrderReply, |r: &SendOrderReq| self
.do_send_order(r));
arm!(CancelOrderReq, CancelOrderReply, |r: &CancelOrderReq| self
.do_cancel_order(r));
arm!(QueryRateReq, QueryRateReply, |r: &QueryRateReq| self
.do_query_rate(r));
arm!(ConfigureReq, ConfigureReply, |r: &ConfigureReq| self
.do_configure(r));
let _ = env
.reply
.send(Box::new(Err::<(), ReplyErr>(ReplyErr::InvalidPayload)));
}
fn do_subscribe(&self, r: &SubscribeReq) -> SubscribeReply {
let mut outs = Vec::new();
{
let mut md = self.md.lock().unwrap();
for s in &r.symbols {
outs.extend(md.subscribe(s));
}
}
for o in outs {
self.emit_md(o);
}
SubscribeReply {
ok: true,
msg: String::new(),
}
}
fn do_send_order(&self, r: &SendOrderReq) -> SendOrderReply {
let outcome = {
let mut td = self.td.lock().unwrap();
td.send_order(&r.req)
};
if let Some(log) = &outcome.error {
self.put_log(log.clone());
}
let mut submit_order: Option<ek_data::order::OrderData> = None;
let mut send_failed: Option<String> = None;
for o in outcome.outs {
match o {
TdOut::Insert(input) => match &self.trans {
Some(t) => {
let n = t.td_insert(&input);
if n != 0 {
send_failed = Some(format!("委托请求发送失败,错误代码:{n}"));
}
}
None => self.warn_no_transport(),
},
TdOut::Order(od) => submit_order = Some(od),
other => self.emit_td(other),
}
}
if let Some(msg) = send_failed {
let log = LogData {
gateway_name: self.gw.clone(),
msg: msg.clone(),
level: 20,
};
self.put_log(log);
return SendOrderReply {
ok: false,
en_orderid: String::new(),
msg,
};
}
if let Some(od) = submit_order {
self.emit_td(TdOut::Order(od));
}
if outcome.en_orderid.is_empty() {
let msg = outcome.error.map(|l| l.msg).unwrap_or_default();
SendOrderReply {
ok: false,
en_orderid: String::new(),
msg,
}
} else if self.trans.is_none() {
self.warn_no_transport();
SendOrderReply {
ok: false,
en_orderid: outcome.en_orderid,
msg: "传输层未接入(默认构建)".to_string(),
}
} else {
SendOrderReply {
ok: true,
en_orderid: outcome.en_orderid,
msg: String::new(),
}
}
}
fn do_query_rate(&self, r: &QueryRateReq) -> QueryRateReply {
let broker = self.broker.lock().unwrap().clone();
let user = self.user.lock().unwrap().clone();
let is_margin = r.kind == "margin";
let is_commission = r.kind == "commission";
if !is_margin && !is_commission {
self.put_log(LogData {
gateway_name: self.gw.clone(),
msg: format!("费率查询类型未知:{}", r.kind),
level: 20,
});
return QueryRateReply {
ok: false,
msg: format!("费率查询类型未知:{}", r.kind),
};
}
if let Some(t) = &self.trans {
let n = if is_margin {
let query = crate::wire::QryInstrumentMarginRate {
broker_id: broker.clone(),
investor_id: user.clone(),
hedge_flag: r.hedge_flag.clone(),
exchange_id: r.exchange_id.clone(),
invest_unit_id: String::new(),
instrument_id: r.instrument_id.clone(),
};
t.td_qry_margin_rate(&query)
} else {
let query = crate::wire::QryInstrumentCommissionRate {
broker_id: broker.clone(),
investor_id: user.clone(),
exchange_id: r.exchange_id.clone(),
invest_unit_id: String::new(),
instrument_id: r.instrument_id.clone(),
};
t.td_qry_commission_rate(&query)
};
let label = if is_margin {
"保证金率"
} else {
"手续费率"
};
if n != 0 {
std::thread::sleep(std::time::Duration::from_millis(1050));
let n2 = if is_margin {
let query = crate::wire::QryInstrumentMarginRate {
broker_id: broker.clone(),
investor_id: user.clone(),
hedge_flag: r.hedge_flag.clone(),
exchange_id: r.exchange_id.clone(),
invest_unit_id: String::new(),
instrument_id: r.instrument_id.clone(),
};
t.td_qry_margin_rate(&query)
} else {
let query = crate::wire::QryInstrumentCommissionRate {
broker_id: broker.clone(),
investor_id: user.clone(),
exchange_id: r.exchange_id.clone(),
invest_unit_id: String::new(),
instrument_id: r.instrument_id.clone(),
};
t.td_qry_commission_rate(&query)
};
let n = n2;
if n != 0 {
let msg = format!("{label}查询请求发送失败,错误代码:{n}(重发一次仍拒)");
self.put_log(LogData {
gateway_name: self.gw.clone(),
msg: msg.clone(),
level: 20,
});
return QueryRateReply { ok: false, msg };
}
QueryRateReply {
ok: true,
msg: format!("{label}查询首次被拒(错误代码另附),重发后受理"),
}
} else {
QueryRateReply {
ok: true,
msg: String::new(),
}
}
} else {
self.warn_no_transport();
QueryRateReply {
ok: false,
msg: "传输层未接入(默认构建)".to_string(),
}
}
}
fn do_cancel_order(&self, r: &CancelOrderReq) -> CancelOrderReply {
let outs = {
let mut td = self.td.lock().unwrap();
td.cancel_order(&r.req)
};
let mut rejected_log: Option<LogData> = None;
let outs: Vec<TdOut> = outs
.into_iter()
.filter(|o| match o {
TdOut::Log(l) => {
rejected_log = Some(l.clone());
false
}
_ => true,
})
.collect();
if let Some(l) = rejected_log {
let msg = l.msg.clone();
self.put_log(l);
return CancelOrderReply { ok: false, msg };
}
for o in outs {
self.emit_td(o);
}
if self.trans.is_none() {
self.warn_no_transport();
return CancelOrderReply {
ok: false,
msg: "传输层未接入(默认构建)".to_string(),
};
}
CancelOrderReply {
ok: true,
msg: String::new(),
}
}
fn do_configure(&self, r: &ConfigureReq) -> ConfigureReply {
self.configure(r)
}
fn effective_classes(&self) -> Vec<String> {
self.td.lock().unwrap().product_classes()
}
pub fn configure(&self, r: &ConfigureReq) -> ConfigureReply {
if r.td_addr.is_empty() && r.md_addr.is_empty() {
return ConfigureReply {
ok: false,
msg: "配置校验: td_addr/md_addr 至少一个非空".to_string(),
product_classes: self.effective_classes(),
};
}
if !r.td_addr.is_empty()
&& (r.broker_id.is_empty() || r.td_user.is_empty() || r.td_password.is_empty())
{
return ConfigureReply {
ok: false,
msg: "配置校验: TD 侧需 broker_id/td_user/td_password 齐".to_string(),
product_classes: self.effective_classes(),
};
}
let md_user = if !r.md_user.is_empty() {
r.md_user.clone()
} else {
r.td_user.clone()
};
let md_password = if !r.md_password.is_empty() {
r.md_password.clone()
} else {
r.td_password.clone()
};
if !r.md_addr.is_empty() && (md_user.is_empty() || md_password.is_empty()) {
return ConfigureReply {
ok: false,
msg: "配置校验: MD 侧需 md_user/md_password(或 td 侧回落)".to_string(),
product_classes: self.effective_classes(),
};
}
let bad_codes: Vec<String> = r
.product_classes
.iter()
.map(|c| c.trim().to_string())
.filter(|c| !c.is_empty() && c.chars().count() != 1)
.collect();
if !bad_codes.is_empty() {
return ConfigureReply {
ok: false,
msg: format!(
"配置校验: product_classes 需为单字符 CTP 码(非法项:{})",
bad_codes.join(",")
),
product_classes: self.effective_classes(),
};
}
let classes = crate::map::ProductClasses(r.product_classes.clone());
let effective_classes = classes.normalize();
{
self.td.lock().unwrap().reconfigure(driver::TdConfig {
addr: r.td_addr.clone(),
user: r.td_user.clone(),
password: r.td_password.clone(),
broker: r.broker_id.clone(),
auth_code: r.td_auth_code.clone(),
app_id: r.td_app_id.clone(),
product_classes: classes.clone(),
});
}
{
self.md.lock().unwrap().reconfigure(driver::MdConfig {
addr: r.md_addr.clone(),
user: md_user,
password: md_password,
broker: r.broker_id.clone(),
});
}
*self.broker.lock().unwrap() = r.broker_id.clone();
*self.user.lock().unwrap() = r.td_user.clone();
*self.cfg.lock().unwrap() = CtpConfig {
gateway_name: self.gw.clone(),
broker_id: r.broker_id.clone(),
td_addr: r.td_addr.clone(),
td_user: r.td_user.clone(),
td_password: r.td_password.clone(),
td_auth_code: r.td_auth_code.clone(),
td_app_id: r.td_app_id.clone(),
md_addr: r.md_addr.clone(),
md_user: r.md_user.clone(),
md_password: r.md_password.clone(),
product_classes: classes,
};
if let Some(t) = &self.trans {
t.td_release();
t.md_release();
}
self.connect();
ConfigureReply {
ok: true,
msg: format!(
"已配置并发起连接(先 TD 后 MD);收录产品类:{}",
effective_classes.join(" ")
),
product_classes: effective_classes,
}
}
}
impl Default for CtpPlugin {
fn default() -> Self {
Self::new(CtpConfig::default())
}
}
impl Plugin for CtpPlugin {
fn name(&self) -> &str {
&self.gw
}
fn routes(&self) -> Vec<Cow<'static, str>> {
vec![Cow::Owned(e_req_type(&self.gw)), Cow::Borrowed(ROUTE_TIMER)]
}
fn version(&self) -> Cow<'static, str> {
Cow::Borrowed(env!("CARGO_PKG_VERSION"))
}
fn services(&self) -> Vec<ServiceDesc> {
vec![
ServiceDesc::describe::<SubscribeReq, SubscribeReply>(
format!("{}.subscribe", self.gw),
"登记并(已登录时)下发行情订阅",
),
ServiceDesc::describe::<SendOrderReq, SendOrderReply>(
format!("{}.send_order", self.gw),
"下单(InputOrder + SUBMITTING 单已上总线)",
),
ServiceDesc::describe::<CancelOrderReq, CancelOrderReply>(
format!("{}.cancel_order", self.gw),
"撤单(三段订单号解析)",
),
ServiceDesc::describe::<QueryRateReq, QueryRateReply>(
format!("{}.query_rate", self.gw),
"费率查询(margin=保证金率/commission=手续费率;状态为纯被动推送)",
),
ServiceDesc::describe::<ConfigureReq, ConfigureReply>(
format!("{}.configure", self.gw),
"网关配置(插件自带配置面;手工输入账号/前置 → 复位守卫并发起连接)",
),
]
}
fn on_init(&self, ctx: &PluginContext) {
let _ = self.ctx.set(ctx.clone());
}
fn on_start(&self) {
self.connect();
}
fn on_stop(&self) {
if let Some(t) = &self.trans {
t.shutdown();
}
}
fn on_mail(&self, event: &Event) {
if event.event_type == ROUTE_TIMER {
self.on_timer();
return;
}
if event.event_type == e_req_type(&self.gw) {
self.on_request(event);
}
}
}