1use crate::driver::{self, MdDriver, MdEvent, MdOut, TdDriver, TdEvent, TdOut};
20use crate::wire::InputOrder;
21use ek_core::context::PluginContext;
22use ek_core::envelope::{Envelope, ReplyErr, e_req_type};
23use ek_core::plugin::Plugin;
24use ek_core::section::{ApplyReport, Configurable, Field, SectionMap};
25use ek_core::service::ServiceDesc;
26use ek_data::cancel_request::CancelRequest;
27use ek_data::contract::ContractData;
28use ek_data::log::LogData;
29#[cfg(feature = "native")]
31use ek_data::log::LEVEL_ERROR;
32use ek_data::log::LEVEL_INFO;
34use ek_data::order_request::OrderRequest;
35use ek_event::Event;
36use std::borrow::Cow;
37use std::sync::atomic::{AtomicU32, Ordering};
38use std::sync::{Arc, Mutex, OnceLock};
39
40const W_TICK: &str = "eTick.";
43const W_ORDER: &str = "eOrder.";
44const W_TRADE: &str = "eTrade.";
45const W_POSITION: &str = "ePosition.";
46const W_ACCOUNT: &str = "eAccount.";
47const W_CONTRACT: &str = "eContract.";
48
49pub const CONTRACT_BATCH_ROWS: usize = 2000;
58const W_POS_DONE: &str = "ePositionDone.";
61const W_ACCOUNT_DONE: &str = "eAccountDone.";
63pub const EVENT_CONTRACT_FINISH: &str = "eContractFinish";
66pub const SVC_STATUS: &str = "status";
69use ek_data::event::EVENT_LOG as W_LOG;
71pub use ek_event::EVENT_TIMER as ROUTE_TIMER;
74
75#[derive(Debug, Clone)]
79pub struct SubscribeReq {
80 pub symbols: Vec<String>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct SubscribeReply {
87 pub ok: bool,
89 pub msg: String,
91}
92
93#[derive(Debug, Clone)]
95pub struct SendOrderReq {
96 pub req: OrderRequest,
98}
99
100#[derive(Debug, Clone, PartialEq)]
102pub struct SendOrderReply {
103 pub ok: bool,
105 pub en_orderid: String,
107 pub msg: String,
109}
110
111#[derive(Debug, Clone)]
113pub struct CancelOrderReq {
114 pub req: CancelRequest,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct CancelOrderReply {
121 pub ok: bool,
123 pub msg: String,
125}
126
127#[derive(Debug, Clone)]
132pub struct QueryRateReq {
133 pub kind: String,
135 pub instrument_id: String,
137 pub hedge_flag: String,
139 pub exchange_id: String,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct QueryRateReply {
146 pub ok: bool,
148 pub msg: String,
150}
151
152#[derive(Clone, Default)]
162pub struct ConfigureReq {
163 pub broker_id: String,
165 pub td_addr: String,
167 pub td_user: String,
169 pub td_password: String,
171 pub td_auth_code: String,
173 pub td_app_id: String,
175 pub md_addr: String,
177 pub md_user: String,
179 pub md_password: String,
181 pub product_classes: Vec<String>,
184}
185
186impl std::fmt::Debug for ConfigureReq {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("ConfigureReq")
189 .field("broker_id", &self.broker_id)
190 .field("td_addr", &self.td_addr)
191 .field("td_password", &"****")
193 .field("td_auth_code", &self.td_auth_code)
194 .field("td_app_id", &self.td_app_id)
195 .field("md_addr", &self.md_addr)
196 .field("md_user", &self.md_user)
197 .field("md_password", &"****")
198 .field("product_classes", &self.product_classes)
199 .finish()
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct ConfigureReply {
206 pub ok: bool,
208 pub msg: String,
210 pub product_classes: Vec<String>,
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq)]
218pub struct StatusReq;
219
220#[derive(Debug, Clone, Default, PartialEq, Eq)]
232pub struct GatewayStatus {
233 pub gateway: String,
235 pub sampled_ms: i64,
237 pub td_handle_up: bool,
240 pub td_logged_in: bool,
242 pub td_login_failed: bool,
244 pub td_auth_ok: bool,
246 pub td_auth_failed: bool,
248 pub td_front_id: i64,
250 pub td_session_id: i64,
252 pub td_disconnects: u32,
254 pub td_last_login_ok_ms: Option<i64>,
256 pub td_last_account_ms: Option<i64>,
258 pub td_last_position_ms: Option<i64>,
260 pub td_query_rejects: u32,
262 pub td_query_reject_streak: u32,
264 pub td_instrument_rejects: u32,
266 pub td_last_error: Option<String>,
268 pub md_handle_up: bool,
271 pub md_logged_in: bool,
273 pub md_disconnects: u32,
275 pub md_last_login_ok_ms: Option<i64>,
277 pub md_last_tick_ms: Option<i64>,
279 pub md_sub_rejects: u32,
281 pub md_last_error: Option<String>,
283 pub md_subscribed: usize,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum GatewayChange {
298 TdConnected,
300 TdDisconnected,
302 TdAuthOk,
304 TdAuthFailed,
306 TdLoginOk,
308 TdLoginFailed,
310 SettlementConfirmed,
312 ContractArrived,
314 ContractQueryStopped,
316 QueryRejected,
318 MdConnected,
320 MdDisconnected,
322 MdLoginOk,
324 MdLoginFailed,
326 SubscribeRejected,
328}
329
330impl GatewayChange {
331 pub fn kind(&self) -> &'static str {
333 match self {
334 Self::TdConnected => "td-connected",
335 Self::TdDisconnected => "td-disconnected",
336 Self::TdAuthOk => "td-auth-ok",
337 Self::TdAuthFailed => "td-auth-failed",
338 Self::TdLoginOk => "td-login-ok",
339 Self::TdLoginFailed => "td-login-failed",
340 Self::SettlementConfirmed => "settlement-confirmed",
341 Self::ContractArrived => "contract-arrived",
342 Self::ContractQueryStopped => "contract-query-stopped",
343 Self::QueryRejected => "query-rejected",
344 Self::MdConnected => "md-connected",
345 Self::MdDisconnected => "md-disconnected",
346 Self::MdLoginOk => "md-login-ok",
347 Self::MdLoginFailed => "md-login-failed",
348 Self::SubscribeRejected => "subscribe-rejected",
349 }
350 }
351
352 pub fn describe(&self, gw: &str, st: &GatewayStatus) -> String {
358 let p = driver::CHAIN_STATE_PREFIX;
359 match self {
360 Self::TdConnected => format!("{p}{gw} 交易前置已连接(下一步:认证)"),
361 Self::TdDisconnected => format!(
362 "{p}{gw} 交易前置断开(累计 {} 次;会话作废,查询阶段回 Idle)",
363 st.td_disconnects
364 ),
365 Self::TdAuthOk => format!("{p}{gw} 授权通过(下一步:登录)"),
366 Self::TdAuthFailed => format!(
367 "{p}{gw} 授权失败:{}",
368 st.td_last_error.as_deref().unwrap_or("无错误文本")
369 ),
370 Self::TdLoginOk => format!(
371 "{p}{gw} 登录成功(Front {} / Session {})",
372 st.td_front_id, st.td_session_id
373 ),
374 Self::TdLoginFailed => format!(
375 "{p}{gw} 登录失败:{}",
376 st.td_last_error.as_deref().unwrap_or("无错误文本")
377 ),
378 Self::SettlementConfirmed => {
379 format!("{p}{gw} 结算单已确认(开始问合约)")
380 }
381 Self::ContractArrived => {
382 format!("{p}{gw} 合约已到手,开始资金/持仓轮询")
383 }
384 Self::ContractQueryStopped => {
385 format!("{p}{gw} 合约查询问满:本轮查询已停止(不再问资金与持仓)")
386 }
387 Self::QueryRejected => format!(
388 "{p}{gw} 查询被拒(连续 {} 次 / 累计 {} 次):{}",
389 st.td_query_reject_streak,
390 st.td_query_rejects,
391 st.td_last_error.as_deref().unwrap_or("无错误文本")
392 ),
393 Self::MdConnected => format!("{p}{gw} 行情前置已连接(下一步:登录)"),
394 Self::MdDisconnected => format!("{p}{gw} 行情前置断开(累计 {} 次)", st.md_disconnects),
395 Self::MdLoginOk => format!(
396 "{p}{gw} 行情登录成功(已登记订阅 {} 项,自会重推)",
397 st.md_subscribed
398 ),
399 Self::MdLoginFailed => format!(
400 "{p}{gw} 行情登录失败:{}",
401 st.md_last_error.as_deref().unwrap_or("无错误文本")
402 ),
403 Self::SubscribeRejected => format!(
404 "{p}{gw} 行情订阅被拒(累计 {} 次):{}",
405 st.md_sub_rejects,
406 st.md_last_error.as_deref().unwrap_or("无错误文本")
407 ),
408 }
409 }
410}
411
412#[derive(Debug, Clone)]
419pub struct GatewayStatusEvent {
420 pub gateway: String,
422 pub at_ms: i64,
424 pub change: GatewayChange,
426 pub status: Box<GatewayStatus>,
428}
429
430impl GatewayStatusEvent {
431 pub fn describe(&self) -> String {
433 self.change.describe(&self.gateway, &self.status)
434 }
435}
436
437pub const EVENT_STATUS_PREFIX: &str = "eStatus.";
439
440#[derive(Debug, Clone, Default, PartialEq, Eq)]
442pub struct DisconnectReq;
443
444#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct DisconnectReply {
447 pub ok: bool,
449 pub msg: String,
451}
452
453pub trait CtpTransport: Send + Sync {
457 fn md_connect(&self, addr: &str, flow_path: &str);
459 fn md_login(&self, user: &str, password: &str, broker: &str);
461 fn md_subscribe(&self, symbols: &[String]);
463 fn td_connect(&self, addr: &str, flow_path: &str);
465 fn td_auth(&self, user: &str, broker: &str, auth_code: &str, app_id: &str);
467 fn td_login(&self, user: &str, password: &str, broker: &str);
469 fn td_settlement_confirm(&self, broker: &str, user: &str);
471 fn td_qry_instrument(&self) -> i32;
476 fn td_qry_account(&self, broker: &str, user: &str) -> i32;
480 fn td_qry_position(&self, broker: &str, user: &str) -> i32;
482 fn td_insert(&self, order: &InputOrder) -> i32;
485 fn td_cancel(&self, action: &crate::wire::InputOrderAction) -> i32;
487 fn td_qry_margin_rate(&self, query: &crate::wire::QryInstrumentMarginRate) -> i32;
489 fn td_qry_commission_rate(&self, query: &crate::wire::QryInstrumentCommissionRate) -> i32;
491
492 fn shutdown(&self) {}
500
501 fn td_release(&self) {}
504 fn md_release(&self) {}
506}
507
508#[derive(Clone, Default)]
516pub struct CtpConfig {
517 pub gateway_name: String,
519 pub broker_id: String,
521 pub td_addr: String,
523 pub td_user: String,
525 pub td_password: String,
527 pub td_auth_code: String,
529 pub td_app_id: String,
531 pub md_addr: String,
533 pub md_user: String,
535 pub md_password: String,
537 pub product_classes: crate::map::ProductClasses,
540}
541
542impl std::fmt::Debug for CtpConfig {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.debug_struct("CtpConfig")
545 .field("gateway_name", &self.gateway_name)
546 .field("broker_id", &self.broker_id)
547 .field("td_addr", &self.td_addr)
548 .field("td_user", &self.td_user)
549 .field("td_password", &"****")
551 .field("td_auth_code", &self.td_auth_code)
552 .field("td_app_id", &self.td_app_id)
553 .field("md_addr", &self.md_addr)
554 .field("md_user", &self.md_user)
555 .field("md_password", &"****")
556 .field("product_classes", &self.product_classes)
557 .finish()
558 }
559}
560
561impl CtpConfig {
562 pub fn gateway_name_or_default(&self) -> String {
564 if self.gateway_name.is_empty() {
565 "ctp".to_string()
566 } else {
567 self.gateway_name.clone()
568 }
569 }
570}
571
572#[derive(Debug, Clone, PartialEq, Eq)]
574pub struct CredentialEntry {
575 pub field: String,
577 pub state: String,
580 pub ref_key: String,
582}
583
584#[derive(Debug, Clone, Default, PartialEq, Eq)]
586pub struct CredentialStateReq;
587
588#[must_use]
605pub fn derived_gateway_name(broker_id: &str, account: &str) -> Option<String> {
606 let broker = sanitize_id_field(broker_id)?;
607 let account = sanitize_id_field(account)?;
608 let name = format!("ctp_{broker}_{account}");
609 if name.len() > 32 {
611 return None;
612 }
613 Some(name)
614}
615
616fn sanitize_id_field(raw: &str) -> Option<String> {
621 let trimmed = raw.trim();
622 if trimmed.is_empty() || !trimmed.chars().all(|c| c.is_ascii_alphanumeric()) {
623 return None;
624 }
625 Some(trimmed.to_ascii_lowercase())
626}
627
628#[must_use]
633pub fn is_derived_gateway_name(name: &str) -> bool {
634 let Some(rest) = name.strip_prefix("ctp_") else {
635 return false;
636 };
637 if name.len() > 32 {
638 return false;
639 }
640 let ok = |s: &str| {
641 !s.is_empty()
642 && s.chars()
643 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
644 };
645 match rest.split_once('_') {
646 Some((broker, account)) => ok(broker) && ok(account),
647 None => false,
648 }
649}
650
651fn lock_clone<T: Clone>(l: &std::sync::Mutex<T>) -> T {
655 match l.lock() {
656 Ok(g) => g.clone(),
657 Err(e) => e.into_inner().clone(),
658 }
659}
660
661pub struct CtpPlugin {
663 gw: String,
664 ns: String,
666 cfg_rev: AtomicU32,
668 sec_refs: Mutex<Vec<(String, String)>>,
671 cfg: Mutex<CtpConfig>,
673 broker: Mutex<String>,
675 user: Mutex<String>,
677 md: Mutex<MdDriver>,
678 td: Mutex<TdDriver>,
679 timer_count: Mutex<u32>,
681 trans: Option<Box<dyn CtpTransport>>,
683 ctx: OnceLock<PluginContext>,
684 trans_warned: Mutex<bool>,
686 save_fallback: Mutex<bool>,
688 contract_batch: Mutex<Vec<ContractData>>,
691}
692
693impl CtpPlugin {
694 pub fn new(cfg: CtpConfig) -> Self {
696 let gw = cfg.gateway_name_or_default();
697 Self::with_parts(cfg, gw, None, None)
698 }
699
700 pub fn with_transport(cfg: CtpConfig, trans: Box<dyn CtpTransport>) -> Self {
702 let gw = cfg.gateway_name_or_default();
703 Self::with_parts(cfg, gw, Some(trans), None)
704 }
705
706 pub fn with_transport_and_clock(
713 cfg: CtpConfig,
714 trans: Box<dyn CtpTransport>,
715 now_ms: Arc<dyn Fn() -> u64 + Send + Sync>,
716 ) -> Self {
717 let gw = cfg.gateway_name_or_default();
718 Self::with_parts(cfg, gw, Some(trans), Some(now_ms))
719 }
720
721 fn with_parts(
722 cfg: CtpConfig,
723 gw: String,
724 trans: Option<Box<dyn CtpTransport>>,
725 clock: Option<Arc<dyn Fn() -> u64 + Send + Sync>>,
726 ) -> Self {
727 let contracts = driver::contracts_empty();
728 let cfg_broker = cfg.broker_id.clone();
729 let cfg_user = cfg.td_user.clone();
730 let md_cfg = driver::MdConfig {
731 addr: cfg.md_addr.clone(),
732 user: cfg.md_user.clone(),
733 password: cfg.md_password.clone(),
734 broker: cfg.broker_id.clone(),
735 };
736 let td_cfg = driver::TdConfig {
737 addr: cfg.td_addr.clone(),
738 user: cfg.td_user.clone(),
739 password: cfg.td_password.clone(),
740 broker: cfg.broker_id.clone(),
741 auth_code: cfg.td_auth_code.clone(),
742 app_id: cfg.td_app_id.clone(),
743 product_classes: cfg.product_classes.clone(),
744 };
745 let md = Mutex::new(MdDriver::new(
747 &gw,
748 md_cfg,
749 contracts.clone(),
750 crate::map::shanghai_now_date(),
751 ));
752 let td = Mutex::new(match clock {
753 Some(f) => TdDriver::with_clock(&gw, td_cfg, contracts.clone(), f),
754 None => TdDriver::new(&gw, td_cfg, contracts.clone()),
755 });
756 let ns = if gw == "ctp" {
757 "ctp".to_owned()
759 } else {
760 format!("ctp.{gw}")
762 };
763 Self {
764 gw,
765 ns,
766 cfg_rev: AtomicU32::new(0),
767 sec_refs: Mutex::new(Vec::new()),
768 cfg: Mutex::new(cfg),
769 broker: Mutex::new(cfg_broker),
770 user: Mutex::new(cfg_user),
771 md,
772 td,
773 timer_count: Mutex::new(0),
774 trans,
775 ctx: OnceLock::new(),
776 trans_warned: Mutex::new(false),
777 save_fallback: Mutex::new(false),
778 contract_batch: Mutex::new(Vec::with_capacity(CONTRACT_BATCH_ROWS)),
779 }
780 }
781
782 #[cfg(feature = "native")]
791 pub fn with_native(cfg: CtpConfig, flow_root: impl Into<std::path::PathBuf>) -> Arc<Self> {
792 let (trans, ingress) = crate::native::transport::native_parts(flow_root);
793 let plugin = Arc::new(Self::with_transport(cfg, trans));
794 crate::native::transport::attach_plugin(&ingress, &plugin);
795 plugin
796 }
797
798 pub fn connect(&self) {
808 let (td_empty, md_empty) = {
811 let c = self.cfg.lock().unwrap();
812 (c.td_addr.is_empty(), c.md_addr.is_empty())
813 };
814 if td_empty && md_empty {
815 self.put_log(LogData {
816 gateway_name: self.gw.clone(),
817 msg: "未配置前置地址,跳过连接(等待 configure)".to_string(),
818 level: 20,
819 });
820 return;
821 }
822 let td_outs = if td_empty {
826 Vec::new()
827 } else {
828 self.td.lock().unwrap().connect()
829 };
830 let md_outs = if md_empty {
831 Vec::new()
832 } else {
833 self.md.lock().unwrap().connect()
834 };
835 for o in td_outs {
836 self.emit_td(o);
837 }
838 for o in md_outs {
839 self.emit_md(o);
840 }
841 }
842
843 #[cfg(feature = "native")]
849 pub(crate) fn transport_log(&self, msg: String) {
850 self.put_log(LogData {
851 gateway_name: self.gw.clone(),
852 msg,
853 level: LEVEL_ERROR,
854 });
855 }
856
857 pub fn on_md_event(&self, ev: MdEvent) {
859 let outs = self.md.lock().unwrap().step(ev);
860 for o in outs {
861 self.emit_md(o);
862 }
863 }
864
865 pub fn on_td_event(&self, ev: TdEvent) {
867 let outs = self.td.lock().unwrap().step(ev);
868 for o in outs {
869 self.emit_td(o);
870 }
871 }
872
873 pub fn on_timer(&self) {
880 let outs = self.td.lock().unwrap().on_timer();
881 for o in outs {
882 self.emit_td(o);
883 }
884 let gate_pass = {
885 let mut c = self.timer_count.lock().unwrap();
886 *c += 1;
887 if *c < 2 {
888 false
889 } else {
890 *c = 0;
891 true
892 }
893 };
894 if !gate_pass {
895 return;
896 }
897 let date = crate::map::shanghai_now_date();
899 self.md.lock().unwrap().update_date(date);
900 }
901
902 fn emit_md(&self, o: MdOut) {
903 match o {
904 MdOut::Log(l) => self.put_log(l),
905 MdOut::Status(ch) => self.publish_status(ch),
907 MdOut::Tick(t) => self.put_data(W_TICK, t),
908 MdOut::ReqConnect { addr, flow_path } => {
909 if let Some(t) = &self.trans {
910 t.md_connect(&addr, &flow_path);
911 } else {
912 self.warn_no_transport();
913 }
914 }
915 MdOut::ReqLogin {
916 user,
917 password,
918 broker,
919 } => {
920 if let Some(t) = &self.trans {
921 t.md_login(&user, &password, &broker);
922 } else {
923 self.warn_no_transport();
924 }
925 }
926 MdOut::ReqSubscribe(symbols) => {
927 if let Some(t) = &self.trans {
928 t.md_subscribe(&symbols);
929 } else {
930 self.warn_no_transport();
931 }
932 }
933 }
934 }
935
936 fn emit_td(&self, o: TdOut) {
937 match o {
938 TdOut::Log(l) => self.put_log(l),
939 TdOut::Status(ch) => self.publish_status(ch),
941 TdOut::Contract(c) => self.emit_contract(c),
942 TdOut::Order(o) => self.publish_enriched(W_ORDER, o),
943 TdOut::Trade(t) => self.publish_enriched(W_TRADE, t),
944 TdOut::Position(p) => self.publish_enriched(W_POSITION, p),
945 TdOut::Account(a) => self.put_data(W_ACCOUNT, a),
946 TdOut::PositionQueryDone(gw) => {
947 if let Some(ctx) = self.ctx.get() {
950 let _ = ctx.put(Event::with_data(W_POS_DONE, Arc::new(gw)));
951 }
952 }
953 TdOut::AccountQueryDone(gw) => {
954 if let Some(ctx) = self.ctx.get() {
956 let _ = ctx.put(Event::with_data(W_ACCOUNT_DONE, Arc::new(gw)));
957 }
958 }
959 TdOut::ContractFinish(gw) => {
962 self.flush_contract();
964 if let Some(ctx) = self.ctx.get() {
965 let _ = ctx.put(Event::with_data(EVENT_CONTRACT_FINISH, Arc::new(gw)));
966 }
967 }
968 TdOut::ReqConnect { addr, flow_path } => {
969 if let Some(t) = &self.trans {
970 t.td_connect(&addr, &flow_path);
971 } else {
972 self.warn_no_transport();
973 }
974 }
975 TdOut::ReqAuthenticate {
976 user,
977 broker,
978 auth_code,
979 app_id,
980 } => {
981 if let Some(t) = &self.trans {
982 t.td_auth(&user, &broker, &auth_code, &app_id);
983 } else {
984 self.warn_no_transport();
985 }
986 }
987 TdOut::ReqLogin {
988 user,
989 password,
990 broker,
991 } => {
992 if let Some(t) = &self.trans {
993 t.td_login(&user, &password, &broker);
994 } else {
995 self.warn_no_transport();
996 }
997 }
998 TdOut::ReqSettlementConfirm { broker, user } => {
999 if let Some(t) = &self.trans {
1000 t.td_settlement_confirm(&broker, &user);
1001 } else {
1002 self.warn_no_transport();
1003 }
1004 }
1005 TdOut::ReqQryInstrument => {
1006 if let Some(t) = &self.trans {
1007 let n = t.td_qry_instrument();
1008 for l in self.td.lock().unwrap().note_instrument_send_failure(n) {
1011 self.put_log(l);
1012 }
1013 } else {
1014 self.warn_no_transport();
1015 }
1016 }
1017 TdOut::ReqQryAccount { broker, user } => {
1018 if let Some(t) = &self.trans {
1019 let n = t.td_qry_account(&broker, &user);
1020 for l in self.td.lock().unwrap().note_query_send_failure("资金", n) {
1021 self.put_log(l);
1022 }
1023 } else {
1024 self.warn_no_transport();
1025 }
1026 }
1027 TdOut::ReqQryPosition { broker, user } => {
1028 if let Some(t) = &self.trans {
1029 let n = t.td_qry_position(&broker, &user);
1030 for l in self.td.lock().unwrap().note_query_send_failure("持仓", n) {
1031 self.put_log(l);
1032 }
1033 } else {
1034 self.warn_no_transport();
1035 }
1036 }
1037 TdOut::Insert(o) => {
1038 if let Some(t) = &self.trans {
1039 t.td_insert(&o);
1040 } else {
1041 self.warn_no_transport();
1042 }
1043 }
1044 TdOut::Cancel(a) => {
1045 if let Some(t) = &self.trans {
1046 t.td_cancel(&a);
1047 } else {
1048 self.warn_no_transport();
1049 }
1050 }
1051 }
1052 }
1053
1054 fn put_log(&self, l: LogData) {
1056 if let Some(ctx) = self.ctx.get() {
1057 let _ = ctx.put(Event::with_data(W_LOG, Arc::new(l)));
1058 }
1059 }
1060
1061 fn put_data<T: Send + Sync + 'static>(&self, prefix: &str, payload: T) {
1070 if let Some(ctx) = self.ctx.get() {
1071 let _ = ctx.put(Event::with_data(prefix, Arc::new(payload)));
1072 }
1073 }
1074
1075 fn emit_contract(&self, c: ContractData) {
1087 let mut batch = self.contract_batch.lock().unwrap();
1088 batch.push(c);
1089 if batch.len() >= CONTRACT_BATCH_ROWS {
1090 let payload = std::mem::take(&mut *batch);
1091 drop(batch);
1092 self.put_data(W_CONTRACT, payload);
1093 }
1094 }
1095
1096 pub fn flush_contract(&self) {
1098 let payload = {
1099 let mut batch = self.contract_batch.lock().unwrap();
1100 std::mem::take(&mut *batch)
1101 };
1102 if !payload.is_empty() {
1103 self.put_data(W_CONTRACT, payload);
1104 }
1105 }
1106
1107 fn publish_enriched<T>(&self, line: &str, mut row: T)
1112 where
1113 T: EnrichName + Send + Sync + 'static,
1114 {
1115 if row.name_mut().is_empty() {
1116 let symbol = row.symbol().to_string();
1117 if let Some(n) = self.td.lock().unwrap().contract_name(&symbol) {
1118 *row.name_mut() = n;
1119 }
1120 }
1121 self.put_data(line, row);
1122 }
1123
1124 fn publish_status(&self, change: GatewayChange) {
1125 let st = self.status();
1126 self.put_data(
1127 EVENT_STATUS_PREFIX,
1128 GatewayStatusEvent {
1129 gateway: self.gw.clone(),
1130 at_ms: st.sampled_ms,
1131 change,
1132 status: Box::new(st),
1133 },
1134 );
1135 }
1136
1137 fn warn_no_transport(&self) {
1139 let mut warned = self.trans_warned.lock().unwrap();
1140 if !*warned {
1141 *warned = true;
1142 self.put_log(LogData {
1143 gateway_name: self.gw.clone(),
1144 msg: "传输层未接入(默认构建):连接/查询/发送动作已被吸收".to_string(),
1145 level: 20,
1146 });
1147 }
1148 }
1149
1150 fn on_request(&self, event: &Event) {
1152 let Some(env) = event.data.downcast_ref::<Envelope>() else {
1153 return;
1154 };
1155 macro_rules! arm {
1156 ($ty:ty, $res:ty, $body:expr) => {
1157 if let Some(req) = env.payload.downcast_ref::<$ty>() {
1158 let res: $res = $body(req);
1159 let _ = env.reply.send(Box::new(Ok::<$res, ReplyErr>(res)));
1160 return;
1161 }
1162 };
1163 }
1164 arm!(SubscribeReq, SubscribeReply, |r: &SubscribeReq| self
1165 .do_subscribe(r));
1166 arm!(SendOrderReq, SendOrderReply, |r: &SendOrderReq| self
1167 .do_send_order(r));
1168 arm!(CancelOrderReq, CancelOrderReply, |r: &CancelOrderReq| self
1169 .do_cancel_order(r));
1170 arm!(QueryRateReq, QueryRateReply, |r: &QueryRateReq| self
1171 .do_query_rate(r));
1172 arm!(ConfigureReq, ConfigureReply, |r: &ConfigureReq| self
1173 .do_configure(r));
1174 arm!(StatusReq, GatewayStatus, |r: &StatusReq| self.do_status(r));
1175 arm!(DisconnectReq, DisconnectReply, |r: &DisconnectReq| self
1176 .do_disconnect(r));
1177 arm!(
1179 CredentialStateReq,
1180 Vec<CredentialEntry>,
1181 |_: &CredentialStateReq| self.credentials_state()
1182 );
1183 let _ = env
1185 .reply
1186 .send(Box::new(Err::<(), ReplyErr>(ReplyErr::InvalidPayload)));
1187 }
1188
1189 fn do_subscribe(&self, r: &SubscribeReq) -> SubscribeReply {
1190 let mut outs = Vec::new();
1191 {
1192 let mut md = self.md.lock().unwrap();
1193 for s in &r.symbols {
1194 outs.extend(md.subscribe(s));
1195 }
1196 }
1197 for o in outs {
1198 self.emit_md(o);
1199 }
1200 SubscribeReply {
1201 ok: true,
1202 msg: String::new(),
1203 }
1204 }
1205
1206 fn do_send_order(&self, r: &SendOrderReq) -> SendOrderReply {
1207 let outcome = {
1208 let mut td = self.td.lock().unwrap();
1209 td.send_order(&r.req)
1210 };
1211 if let Some(log) = &outcome.error {
1212 self.put_log(log.clone());
1213 }
1214 let mut submit_order: Option<ek_data::order::OrderData> = None;
1218 let mut send_failed: Option<String> = None;
1219 for o in outcome.outs {
1220 match o {
1221 TdOut::Insert(input) => match &self.trans {
1222 Some(t) => {
1223 let n = t.td_insert(&input);
1224 if n != 0 {
1225 send_failed = Some(format!("委托请求发送失败,错误代码:{n}"));
1226 }
1227 }
1228 None => self.warn_no_transport(),
1229 },
1230 TdOut::Order(od) => submit_order = Some(od),
1231 other => self.emit_td(other),
1232 }
1233 }
1234 if let Some(msg) = send_failed {
1235 let log = LogData {
1236 gateway_name: self.gw.clone(),
1237 msg: msg.clone(),
1238 level: 20,
1239 };
1240 self.put_log(log);
1241 return SendOrderReply {
1242 ok: false,
1243 en_orderid: String::new(),
1244 msg,
1245 };
1246 }
1247 if let Some(od) = submit_order {
1248 self.emit_td(TdOut::Order(od));
1249 }
1250 if outcome.en_orderid.is_empty() {
1251 let msg = outcome.error.map(|l| l.msg).unwrap_or_default();
1252 SendOrderReply {
1253 ok: false,
1254 en_orderid: String::new(),
1255 msg,
1256 }
1257 } else if self.trans.is_none() {
1258 self.warn_no_transport();
1259 SendOrderReply {
1260 ok: false,
1261 en_orderid: outcome.en_orderid,
1262 msg: "传输层未接入(默认构建)".to_string(),
1263 }
1264 } else {
1265 SendOrderReply {
1266 ok: true,
1267 en_orderid: outcome.en_orderid,
1268 msg: String::new(),
1269 }
1270 }
1271 }
1272
1273 fn do_query_rate(&self, r: &QueryRateReq) -> QueryRateReply {
1277 let broker = self.broker.lock().unwrap().clone();
1278 let user = self.user.lock().unwrap().clone();
1279 let is_margin = r.kind == "margin";
1280 let is_commission = r.kind == "commission";
1281 if !is_margin && !is_commission {
1282 self.put_log(LogData {
1283 gateway_name: self.gw.clone(),
1284 msg: format!("费率查询类型未知:{}", r.kind),
1285 level: 20,
1286 });
1287 return QueryRateReply {
1288 ok: false,
1289 msg: format!("费率查询类型未知:{}", r.kind),
1290 };
1291 }
1292 if let Some(t) = &self.trans {
1293 let n = if is_margin {
1294 let query = crate::wire::QryInstrumentMarginRate {
1295 broker_id: broker.clone(),
1296 investor_id: user.clone(),
1297 hedge_flag: r.hedge_flag.clone(),
1298 exchange_id: r.exchange_id.clone(),
1299 invest_unit_id: String::new(),
1300 instrument_id: r.instrument_id.clone(),
1301 };
1302 t.td_qry_margin_rate(&query)
1303 } else {
1304 let query = crate::wire::QryInstrumentCommissionRate {
1305 broker_id: broker.clone(),
1306 investor_id: user.clone(),
1307 exchange_id: r.exchange_id.clone(),
1308 invest_unit_id: String::new(),
1309 instrument_id: r.instrument_id.clone(),
1310 };
1311 t.td_qry_commission_rate(&query)
1312 };
1313 let label = if is_margin {
1314 "保证金率"
1315 } else {
1316 "手续费率"
1317 };
1318 if n != 0 {
1319 let msg =
1330 format!("{label}查询请求发送失败,错误代码:{n}(不自动重发:费率查询按需调用)");
1331 self.put_log(LogData {
1332 gateway_name: self.gw.clone(),
1333 msg: msg.clone(),
1334 level: 20,
1335 });
1336 return QueryRateReply { ok: false, msg };
1337 }
1338 QueryRateReply {
1339 ok: true,
1340 msg: String::new(),
1341 }
1342 } else {
1343 self.warn_no_transport();
1344 QueryRateReply {
1345 ok: false,
1346 msg: "传输层未接入(默认构建)".to_string(),
1347 }
1348 }
1349 }
1350
1351 fn do_cancel_order(&self, r: &CancelOrderReq) -> CancelOrderReply {
1352 let outs = {
1353 let mut td = self.td.lock().unwrap();
1354 td.cancel_order(&r.req)
1355 };
1356 let mut rejected_log: Option<LogData> = None;
1357 let outs: Vec<TdOut> = outs
1358 .into_iter()
1359 .filter(|o| match o {
1360 TdOut::Log(l) => {
1361 rejected_log = Some(l.clone());
1362 false
1363 }
1364 _ => true,
1365 })
1366 .collect();
1367 if let Some(l) = rejected_log {
1368 let msg = l.msg.clone();
1369 self.put_log(l);
1370 return CancelOrderReply { ok: false, msg };
1371 }
1372 for o in outs {
1373 self.emit_td(o);
1374 }
1375 if self.trans.is_none() {
1376 self.warn_no_transport();
1377 return CancelOrderReply {
1378 ok: false,
1379 msg: "传输层未接入(默认构建)".to_string(),
1380 };
1381 }
1382 CancelOrderReply {
1383 ok: true,
1384 msg: String::new(),
1385 }
1386 }
1387
1388 fn do_configure(&self, r: &ConfigureReq) -> ConfigureReply {
1397 self.configure(r)
1398 }
1399
1400 fn do_status(&self, _r: &StatusReq) -> GatewayStatus {
1401 self.status()
1402 }
1403
1404 fn do_disconnect(&self, _r: &DisconnectReq) -> DisconnectReply {
1405 self.disconnect()
1406 }
1407
1408 pub fn status(&self) -> GatewayStatus {
1413 let td = self.td.lock().unwrap().status_view();
1414 let md = self.md.lock().unwrap().status_view();
1415 GatewayStatus {
1416 gateway: self.gw.clone(),
1417 sampled_ms: crate::map::now_epoch_ms(),
1418 td_handle_up: td.handle_up,
1419 td_logged_in: td.logged_in,
1420 td_login_failed: td.login_failed,
1421 td_auth_ok: td.auth_ok,
1422 td_auth_failed: td.auth_failed,
1423 td_front_id: td.front_id,
1424 td_session_id: td.session_id,
1425 td_disconnects: td.disconnects,
1426 td_last_login_ok_ms: td.last_login_ok_ms,
1427 td_last_account_ms: td.last_account_ms,
1428 td_last_position_ms: td.last_position_ms,
1429 td_query_rejects: td.query_rejects,
1430 td_query_reject_streak: td.query_reject_streak,
1431 td_instrument_rejects: td.instrument_rejects,
1432 td_last_error: td.last_error,
1433 md_handle_up: md.handle_up,
1434 md_logged_in: md.logged_in,
1435 md_disconnects: md.disconnects,
1436 md_last_login_ok_ms: md.last_login_ok_ms,
1437 md_last_tick_ms: md.last_tick_ms,
1438 md_sub_rejects: md.sub_rejects,
1439 md_last_error: md.last_error,
1440 md_subscribed: md.subscribed,
1441 }
1442 }
1443
1444 pub fn disconnect(&self) -> DisconnectReply {
1450 if let Some(t) = &self.trans {
1451 t.td_release();
1452 t.md_release();
1453 }
1454 {
1455 self.td.lock().unwrap().reset_session();
1456 }
1457 {
1458 self.md.lock().unwrap().reset_session();
1459 }
1460 self.flush_contract();
1463 self.put_log(LogData {
1464 gateway_name: self.gw.clone(),
1465 msg: "会话已停(实例与订阅保留;『连接』可恢复)".to_string(),
1466 level: LEVEL_INFO,
1467 });
1468 self.put_log(LogData {
1470 gateway_name: self.gw.clone(),
1471 msg: format!("{}会话已复位", driver::CHAIN_STATE_PREFIX),
1472 level: LEVEL_INFO,
1473 });
1474 DisconnectReply {
1475 ok: true,
1476 msg: "会话已停(实例与订阅保留;『连接』可恢复)".to_string(),
1477 }
1478 }
1479
1480 fn effective_classes(&self) -> Vec<String> {
1485 self.td.lock().unwrap().product_classes()
1486 }
1487
1488 pub fn configure(&self, r: &ConfigureReq) -> ConfigureReply {
1491 if r.td_addr.is_empty() && r.md_addr.is_empty() {
1492 return ConfigureReply {
1493 ok: false,
1494 msg: "配置校验: td_addr/md_addr 至少一个非空".to_string(),
1495 product_classes: self.effective_classes(),
1496 };
1497 }
1498 if !r.td_addr.is_empty()
1499 && (r.broker_id.is_empty() || r.td_user.is_empty() || r.td_password.is_empty())
1500 {
1501 return ConfigureReply {
1502 ok: false,
1503 msg: "配置校验: TD 侧需 broker_id/td_user/td_password 齐".to_string(),
1504 product_classes: self.effective_classes(),
1505 };
1506 }
1507 let md_user = if !r.md_user.is_empty() {
1508 r.md_user.clone()
1509 } else {
1510 r.td_user.clone()
1511 };
1512 let md_password = if !r.md_password.is_empty() {
1513 r.md_password.clone()
1514 } else {
1515 r.td_password.clone()
1516 };
1517 if !r.md_addr.is_empty() && (md_user.is_empty() || md_password.is_empty()) {
1518 return ConfigureReply {
1519 ok: false,
1520 msg: "配置校验: MD 侧需 md_user/md_password(或 td 侧回落)".to_string(),
1521 product_classes: self.effective_classes(),
1522 };
1523 }
1524 let bad_codes: Vec<String> = r
1527 .product_classes
1528 .iter()
1529 .map(|c| c.trim().to_string())
1530 .filter(|c| !c.is_empty() && c.chars().count() != 1)
1531 .collect();
1532 if !bad_codes.is_empty() {
1533 return ConfigureReply {
1534 ok: false,
1535 msg: format!(
1536 "配置校验: product_classes 需为单字符 CTP 码(非法项:{})",
1537 bad_codes.join(",")
1538 ),
1539 product_classes: self.effective_classes(),
1540 };
1541 }
1542 let classes = crate::map::ProductClasses(r.product_classes.clone());
1543 let effective_classes = classes.normalize();
1544 {
1545 self.td.lock().unwrap().reconfigure(driver::TdConfig {
1546 addr: r.td_addr.clone(),
1547 user: r.td_user.clone(),
1548 password: r.td_password.clone(),
1549 broker: r.broker_id.clone(),
1550 auth_code: r.td_auth_code.clone(),
1551 app_id: r.td_app_id.clone(),
1552 product_classes: classes.clone(),
1553 });
1554 }
1555 {
1556 self.md.lock().unwrap().reconfigure(driver::MdConfig {
1557 addr: r.md_addr.clone(),
1558 user: md_user,
1559 password: md_password,
1560 broker: r.broker_id.clone(),
1561 });
1562 }
1563 *self.broker.lock().unwrap() = r.broker_id.clone();
1564 *self.user.lock().unwrap() = r.td_user.clone();
1565 *self.cfg.lock().unwrap() = CtpConfig {
1566 gateway_name: self.gw.clone(),
1567 broker_id: r.broker_id.clone(),
1568 td_addr: r.td_addr.clone(),
1569 td_user: r.td_user.clone(),
1570 td_password: r.td_password.clone(),
1571 td_auth_code: r.td_auth_code.clone(),
1572 td_app_id: r.td_app_id.clone(),
1573 md_addr: r.md_addr.clone(),
1574 md_user: r.md_user.clone(),
1575 md_password: r.md_password.clone(),
1576 product_classes: classes,
1577 };
1578 if let Some(t) = &self.trans {
1580 t.td_release();
1581 t.md_release();
1582 }
1583 self.connect();
1584 self.persist_config();
1588 ConfigureReply {
1589 ok: true,
1590 msg: format!(
1591 "已配置并发起连接(先 TD 后 MD);收录产品类:{}",
1592 effective_classes.join(" ")
1593 ),
1594 product_classes: effective_classes,
1595 }
1596 }
1597
1598 #[must_use]
1601 pub fn credentials_state(&self) -> Vec<CredentialEntry> {
1602 let refs = lock_clone(&self.sec_refs);
1603 let cfg = lock_clone(&self.cfg);
1604 let mut out = Vec::with_capacity(2);
1605 for field in ["td_password", "md_password"] {
1606 let sk = refs
1607 .iter()
1608 .find(|(f, _)| f == field)
1609 .map(|(_, k)| k.clone())
1610 .unwrap_or_default();
1611 let resolved = if field == "td_password" {
1612 !cfg.td_password.is_empty()
1613 } else {
1614 !cfg.md_password.is_empty()
1615 };
1616 let state = if resolved {
1617 "RESOLVED"
1618 } else if sk.is_empty() {
1619 "ABSENT"
1620 } else {
1621 "REFERENCED"
1622 };
1623 out.push(CredentialEntry {
1624 field: field.to_owned(),
1625 state: state.to_owned(),
1626 ref_key: sk,
1627 });
1628 }
1629 out
1630 }
1631
1632 fn resolve_pending_secrets(&self) {
1637 let refs = lock_clone(&self.sec_refs);
1638 if refs.is_empty() {
1639 return;
1640 }
1641 let mut cfg = lock_clone(&self.cfg);
1642 let mut hit = false;
1643 for (field, sk) in &refs {
1644 match ek_cfg::secret::get(&self.ns, sk) {
1645 Some(v) => match std::str::from_utf8(v.as_bytes()) {
1646 Ok(text) => {
1647 if field == "td_password" {
1648 cfg.td_password = text.to_owned();
1649 hit = true;
1650 } else if field == "md_password" {
1651 cfg.md_password = text.to_owned();
1652 hit = true;
1653 }
1654 }
1655 Err(_) => {
1656 self.put_log(LogData {
1659 gateway_name: self.gw.clone(),
1660 msg: format!("{field} 的秘值不是文本(键 {sk}),按待手配处理"),
1661 level: 40,
1662 });
1663 }
1664 },
1665 None => {
1666 self.put_log(LogData {
1667 gateway_name: self.gw.clone(),
1668 msg: format!("{field} 的引用在库值缺(键 {sk}),待手配"),
1669 level: 20,
1670 });
1671 }
1672 }
1673 }
1674 if !hit {
1675 return;
1676 }
1677 let td_cfg = driver::TdConfig {
1680 addr: cfg.td_addr.clone(),
1681 user: cfg.td_user.clone(),
1682 password: cfg.td_password.clone(),
1683 broker: cfg.broker_id.clone(),
1684 auth_code: cfg.td_auth_code.clone(),
1685 app_id: cfg.td_app_id.clone(),
1686 product_classes: cfg.product_classes.clone(),
1687 };
1688 let md_cfg = driver::MdConfig {
1689 addr: cfg.md_addr.clone(),
1690 user: if cfg.md_user.is_empty() {
1691 cfg.td_user.clone()
1692 } else {
1693 cfg.md_user.clone()
1694 },
1695 password: if cfg.md_password.is_empty() {
1696 cfg.td_password.clone()
1697 } else {
1698 cfg.md_password.clone()
1699 },
1700 broker: cfg.broker_id.clone(),
1701 };
1702 self.td.lock().unwrap().reconfigure(td_cfg);
1703 self.md.lock().unwrap().reconfigure(md_cfg);
1704 *self.cfg.lock().unwrap() = cfg;
1705 }
1706
1707 fn persist_config(&self) {
1714 let map = self.dump_config();
1717 if map.is_empty() {
1718 return; }
1720 let ns = Configurable::config_ns(self).to_owned();
1721 let req = ek_cfg::wire::SaveReq {
1722 ns: ns.clone(),
1723 map: map.clone(),
1724 };
1725 let ctx = self.ctx.get();
1726 let res = match ctx {
1727 Some(c) => c.ask_service::<ek_cfg::wire::SaveReq, ek_cfg::wire::SaveRes>(
1728 "cfg.save",
1729 req,
1730 std::time::Duration::from_millis(1500),
1731 ),
1732 None => Err(ek_core::error::AskError::UnknownService {
1734 service: "cfg.save".to_owned(),
1735 }),
1736 };
1737 match res {
1738 Ok(r) if r.ok => {
1739 self.put_log(LogData {
1740 gateway_name: self.gw.clone(),
1741 msg: format!("配置已持久化(段 {});警告{:?}", ns, r.warns),
1742 level: 20,
1743 });
1744 }
1745 Ok(r) => {
1746 self.put_log(LogData {
1747 gateway_name: self.gw.clone(),
1748 msg: format!("配置落盘失败(段 {}):{}", ns, r.msg),
1749 level: 40,
1750 });
1751 }
1752 Err(_) => {
1753 let once = {
1755 let mut f = self.save_fallback.lock().unwrap();
1756 let first = !*f;
1757 *f = true;
1758 first
1759 };
1760 if once {
1761 self.put_log(LogData {
1762 gateway_name: self.gw.clone(),
1763 msg: "cfg 缺席,已直写落盘".to_string(),
1764 level: 20,
1765 });
1766 }
1767 let ok = Self::store_direct(&ns, &map);
1768 if !ok {
1769 self.put_log(LogData {
1770 gateway_name: self.gw.clone(),
1771 msg: format!("直写托底也失败(段 {})", ns),
1772 level: 40,
1773 });
1774 }
1775 }
1776 }
1777 }
1778
1779 fn store_direct(ns: &str, map: &SectionMap) -> bool {
1782 let store = match std::env::var("EK_CFG_FALLBACK_ROOT") {
1783 Ok(v) if !v.is_empty() => ek_cfg::Store::with_root(std::path::PathBuf::from(v)),
1784 _ => ek_cfg::Store::eco()
1785 .unwrap_or_else(|_| ek_cfg::Store::with_root("./ekcfg-local".into())),
1786 };
1787 store.store(ns, map).is_ok()
1788 }
1789}
1790
1791impl Configurable for CtpPlugin {
1794 fn config_ns(&self) -> &str {
1795 &self.ns
1796 }
1797
1798 fn apply_config(&self, m: &SectionMap) -> Result<ApplyReport, String> {
1802 if m.rev > self.cfg_rev.load(Ordering::Relaxed) {
1803 self.cfg_rev.store(m.rev, Ordering::Relaxed);
1804 }
1805 let mut rep = ApplyReport::all_adopted();
1806 let mut patch = lock_clone(&self.cfg);
1807 let mut new_refs: Vec<(String, String)> = Vec::new();
1808 for (k, f) in m.iter() {
1809 match k {
1810 "broker_id" => {
1811 patch.broker_id = f.plain_value().unwrap_or("").to_owned();
1812 rep.adopted.push(k.to_owned());
1813 }
1814 "td_addr" => {
1815 patch.td_addr = f.plain_value().unwrap_or("").to_owned();
1816 rep.adopted.push(k.to_owned());
1817 }
1818 "td_user" => {
1819 patch.td_user = f.plain_value().unwrap_or("").to_owned();
1820 rep.adopted.push(k.to_owned());
1821 }
1822 "td_auth_code" => {
1823 patch.td_auth_code = f.plain_value().unwrap_or("").to_owned();
1824 rep.adopted.push(k.to_owned());
1825 }
1826 "td_app_id" => {
1827 patch.td_app_id = f.plain_value().unwrap_or("").to_owned();
1828 rep.adopted.push(k.to_owned());
1829 }
1830 "md_addr" => {
1831 patch.md_addr = f.plain_value().unwrap_or("").to_owned();
1832 rep.adopted.push(k.to_owned());
1833 }
1834 "md_user" => {
1835 patch.md_user = f.plain_value().unwrap_or("").to_owned();
1836 rep.adopted.push(k.to_owned());
1837 }
1838 "product_classes" => {
1839 patch.product_classes = crate::map::ProductClasses(
1840 f.plain_value()
1841 .unwrap_or("")
1842 .split(',')
1843 .map(str::trim)
1844 .filter(|c| !c.is_empty())
1845 .map(str::to_owned)
1846 .collect(),
1847 );
1848 rep.adopted.push(k.to_owned());
1849 }
1850 "td_password" | "md_password" => {
1851 if let Some(sk) = f.secret_key() {
1852 new_refs.push((k.to_owned(), sk.to_owned())); rep.adopted.push(k.to_owned());
1854 } else {
1855 rep.deferred.push(k.to_owned()); }
1857 }
1858 _ => rep.deferred.push(k.to_owned()), }
1860 }
1861 *self.sec_refs.lock().unwrap() = new_refs;
1862 let td_cfg = driver::TdConfig {
1865 addr: patch.td_addr.clone(),
1866 user: patch.td_user.clone(),
1867 password: patch.td_password.clone(),
1868 broker: patch.broker_id.clone(),
1869 auth_code: patch.td_auth_code.clone(),
1870 app_id: patch.td_app_id.clone(),
1871 product_classes: patch.product_classes.clone(),
1872 };
1873 let md_cfg = driver::MdConfig {
1874 addr: patch.md_addr.clone(),
1875 user: if patch.md_user.is_empty() {
1876 patch.td_user.clone()
1877 } else {
1878 patch.md_user.clone()
1879 },
1880 password: if patch.md_password.is_empty() {
1881 patch.td_password.clone()
1882 } else {
1883 patch.md_password.clone()
1884 },
1885 broker: patch.broker_id.clone(),
1886 };
1887 self.td
1888 .lock()
1889 .map_err(|e| e.to_string())?
1890 .reconfigure(td_cfg);
1891 self.md
1892 .lock()
1893 .map_err(|e| e.to_string())?
1894 .reconfigure(md_cfg);
1895 *self.broker.lock().unwrap() = patch.broker_id.clone();
1896 *self.user.lock().unwrap() = patch.td_user.clone();
1897 *self.cfg.lock().unwrap() = patch;
1898 Ok(rep)
1899 }
1900
1901 fn dump_config(&self) -> SectionMap {
1905 let next = self.cfg_rev.load(Ordering::Relaxed) + 1;
1906 self.cfg_rev.store(next, Ordering::Relaxed);
1907 let cfg = lock_clone(&self.cfg);
1908 let mut map = SectionMap::new(next);
1909 for (k, v) in [
1910 ("broker_id", &cfg.broker_id),
1911 ("td_addr", &cfg.td_addr),
1912 ("td_user", &cfg.td_user),
1913 ("td_auth_code", &cfg.td_auth_code),
1914 ("td_app_id", &cfg.td_app_id),
1915 ("md_addr", &cfg.md_addr),
1916 ("md_user", &cfg.md_user),
1917 ] {
1918 if !v.is_empty() {
1919 map = map.set(k, Field::plain(v));
1920 }
1921 }
1922 if !cfg.product_classes.0.is_empty() {
1923 map = map.set(
1924 "product_classes",
1925 Field::plain(cfg.product_classes.0.join(",")),
1926 );
1927 }
1928 for (field, value) in [
1929 ("td_password", &cfg.td_password),
1930 ("md_password", &cfg.md_password),
1931 ] {
1932 let refs = lock_clone(&self.sec_refs);
1933 if let Some((_, sk)) = refs.iter().find(|(f, _)| f == field) {
1934 if !value.is_empty() {
1938 let _ = ek_cfg::secret::set(
1939 &self.ns,
1940 sk,
1941 ek_cfg::SecretValue::from_bytes(value.clone()),
1942 );
1943 }
1944 map = map.set(field, Field::secret(sk.as_str()));
1945 } else if !value.is_empty() {
1946 let sk = format!("gw_{field}");
1948 if ek_cfg::secret::set(
1949 &self.ns,
1950 &sk,
1951 ek_cfg::SecretValue::from_bytes(value.clone()),
1952 )
1953 .is_ok()
1954 {
1955 self.sec_refs
1956 .lock()
1957 .unwrap()
1958 .push((field.to_owned(), sk.clone()));
1959 map = map.set(field, Field::secret(sk.as_str()));
1960 }
1961 }
1963 }
1965 map
1966 }
1967}
1968
1969impl Default for CtpPlugin {
1970 fn default() -> Self {
1971 Self::new(CtpConfig::default())
1972 }
1973}
1974
1975impl Plugin for CtpPlugin {
1976 fn name(&self) -> &str {
1977 &self.gw
1978 }
1979
1980 fn routes(&self) -> Vec<Cow<'static, str>> {
1981 vec![Cow::Owned(e_req_type(&self.gw)), Cow::Borrowed(ROUTE_TIMER)]
1982 }
1983
1984 fn version(&self) -> Cow<'static, str> {
1985 Cow::Borrowed(env!("CARGO_PKG_VERSION"))
1986 }
1987
1988 fn services(&self) -> Vec<ServiceDesc> {
1989 vec![
1990 ServiceDesc::describe::<SubscribeReq, SubscribeReply>(
1991 format!("{}.subscribe", self.gw),
1992 "登记并(已登录时)下发行情订阅",
1993 ),
1994 ServiceDesc::describe::<SendOrderReq, SendOrderReply>(
1995 format!("{}.send_order", self.gw),
1996 "下单(InputOrder + SUBMITTING 单已上总线)",
1997 ),
1998 ServiceDesc::describe::<CancelOrderReq, CancelOrderReply>(
1999 format!("{}.cancel_order", self.gw),
2000 "撤单(三段订单号解析)",
2001 ),
2002 ServiceDesc::describe::<QueryRateReq, QueryRateReply>(
2003 format!("{}.query_rate", self.gw),
2004 "费率查询(margin=保证金率/commission=手续费率;状态为纯被动推送)",
2005 ),
2006 ServiceDesc::describe::<ConfigureReq, ConfigureReply>(
2007 format!("{}.configure", self.gw),
2008 "网关配置(插件自带配置面;手工输入账号/前置 → 复位守卫并发起连接)",
2009 ),
2010 ServiceDesc::describe::<StatusReq, GatewayStatus>(
2011 format!("{}.{}", self.gw, SVC_STATUS),
2012 "状态读数(0.9.0;登录态 / 断开计数 / 查询被拒计数 / 最近回报时刻;只读、不写日志)",
2013 ),
2014 ServiceDesc::describe::<DisconnectReq, DisconnectReply>(
2015 format!("{}.disconnect", self.gw),
2016 "停会话(0.9.0;实例与订阅保留,『连接』可恢复)",
2017 ),
2018 ServiceDesc::describe::<CredentialStateReq, Vec<CredentialEntry>>(
2020 format!("{}.credentials_state", self.gw),
2021 "凭据档位(REFERENCED/RESOLVED/ABSENT;只读)",
2022 ),
2023 ]
2024 }
2025
2026 fn on_init(&self, ctx: &PluginContext) {
2027 let _ = self.ctx.set(ctx.clone());
2028 }
2029
2030 fn on_start(&self) {
2031 self.resolve_pending_secrets();
2035 self.connect();
2038 }
2039
2040 fn on_stop(&self) {
2041 if let Some(t) = &self.trans {
2043 t.shutdown();
2044 }
2045 self.flush_contract();
2047 }
2048
2049 fn on_mail(&self, event: &Event) {
2050 if event.event_type == ROUTE_TIMER {
2051 self.on_timer();
2052 return;
2053 }
2054 if event.event_type == e_req_type(&self.gw) {
2055 self.on_request(event);
2056 }
2057 }
2058
2059 fn as_configurable(&self) -> Option<&dyn Configurable> {
2060 Some(self)
2062 }
2063}
2064
2065trait EnrichName {
2070 fn symbol(&self) -> &str;
2072 fn name_mut(&mut self) -> &mut String;
2074}
2075
2076impl EnrichName for ek_data::order::OrderData {
2077 fn symbol(&self) -> &str {
2078 &self.symbol
2079 }
2080 fn name_mut(&mut self) -> &mut String {
2081 &mut self.name
2082 }
2083}
2084
2085impl EnrichName for ek_data::trade::TradeData {
2086 fn symbol(&self) -> &str {
2087 &self.symbol
2088 }
2089 fn name_mut(&mut self) -> &mut String {
2090 &mut self.name
2091 }
2092}
2093
2094impl EnrichName for ek_data::position::PositionData {
2095 fn symbol(&self) -> &str {
2096 &self.symbol
2097 }
2098 fn name_mut(&mut self) -> &mut String {
2099 &mut self.name
2100 }
2101}