use ek_converter::{ConvertMode, OffsetConverter, PositionHolding};
use ek_core::lifecycle::EVENT_PLUG_UP;
use ek_core::service::PlugNotice;
use ek_core::{
AskError, Envelope, MainEngine, Plugin, PluginContext, PluginError, PluginState, ReplyErr,
ServiceDesc, e_req_type,
};
use ek_data::OrderData;
use ek_data::OrderRequest;
use ek_data::constant::{Asset, Direction, Exchange, Offset, OrderType, Status};
use ek_data::contract::ContractData;
use ek_data::position::PositionData;
use ek_data::trade::TradeData;
use ek_event::{Event, EventEngine};
use ek_oms::plugin::{
CancelCountReq, GetAllActiveOrdersReq, GetAllOrdersReq, GetContractReq, GetPositionReq,
OmsPlugin, OrderCancelCountReq, TradeInfoCountReq,
};
use std::any::Any;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
const W_ORDER: &str = "eOrder.";
const W_TRADE: &str = "eTrade.";
const W_POSITION: &str = "ePosition.";
const W_CONTRACT: &str = "eContract.";
const W_POS_DONE: &str = "ePositionDone.";
const FIXTURE_NAME: &str = "offset";
#[derive(Debug, Clone)]
struct GetHoldingReq {
gateway_name: String,
en_symbol: String,
}
#[derive(Debug, Clone)]
struct ConvertReq {
req: OrderRequest,
gateway_name: String,
lock_mode: LockMode,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LockMode {
Auto,
Lock,
Net,
None,
}
struct OffsetFixture {
conv: Mutex<HashMap<String, OffsetConverter>>,
contracts: Mutex<HashMap<String, ContractData>>,
started: Mutex<HashSet<String>>,
asks: Mutex<u64>,
ctx: OnceLock<PluginContext>,
}
impl OffsetFixture {
fn new() -> Self {
Self {
conv: Mutex::new(HashMap::new()),
contracts: Mutex::new(HashMap::new()),
started: Mutex::new(HashSet::new()),
asks: Mutex::new(0),
ctx: OnceLock::new(),
}
}
fn asks_made(&self) -> u64 {
*self.asks.lock().unwrap()
}
fn resolve(&self, en_symbol: &str) -> Option<ContractData> {
if let Some(c) = self.contracts.lock().unwrap().get(en_symbol) {
return Some(c.clone());
}
let ctx = self.ctx.get()?;
*self.asks.lock().unwrap() += 1;
for _ in 0..2 {
match ctx.ask::<GetContractReq, Option<ContractData>>(
"oms",
GetContractReq {
en_symbol: en_symbol.to_string(),
},
Duration::from_millis(500),
) {
Ok(hit) => {
if let Some(c) = &hit {
self.contracts
.lock()
.unwrap()
.insert(en_symbol.to_string(), c.clone());
self.ensure_converter(c);
}
return hit;
}
Err(AskError::Timeout { .. }) => continue,
Err(_) => return None,
}
}
None
}
fn ensure_converter(&self, c: &ContractData) {
self.conv
.lock()
.unwrap()
.entry(c.gateway_name.clone())
.or_default();
}
fn on_contract(&self, c: &ContractData) {
self.contracts
.lock()
.unwrap()
.insert(c.en_symbol(), c.clone());
self.ensure_converter(c);
}
fn on_order(&self, o: &OrderData) {
let contract = self.resolve(&o.en_symbol());
if let Some(conv) = self.conv.lock().unwrap().get_mut(o.gateway_name.as_str()) {
conv.update_order(o, contract.as_ref());
}
}
fn on_trade(&self, t: &TradeData) {
let contract = self.resolve(&t.en_symbol());
if let Some(conv) = self.conv.lock().unwrap().get_mut(t.gateway_name.as_str()) {
conv.update_trade(t, contract.as_ref());
}
}
fn on_position(&self, p: &PositionData) {
if self
.started
.lock()
.unwrap()
.contains(p.gateway_name.as_str())
{
return;
}
let contract = self.resolve(&p.en_symbol());
if let Some(conv) = self.conv.lock().unwrap().get_mut(p.gateway_name.as_str()) {
conv.update_position(p, contract.as_ref());
}
}
fn on_pos_done(&self, gateway: &str) {
self.started.lock().unwrap().insert(gateway.to_string());
}
fn holding(&self, gateway: &str, en_symbol: &str) -> Option<PositionHolding> {
self.conv
.lock()
.unwrap()
.get(gateway)
.and_then(|c| c.holding(en_symbol).cloned())
}
fn convert(&self, req: OrderRequest, gateway: &str, m: LockMode) -> Vec<OrderRequest> {
if m == LockMode::None {
return vec![req]; }
let contract = self.resolve(&req.en_symbol());
let mut convs = self.conv.lock().unwrap();
let Some(conv) = convs.get_mut(gateway) else {
return vec![req]; };
match m {
LockMode::None => unreachable!("short-circuited above"),
LockMode::Lock => {
conv.convert_order_request(&req, ConvertMode::Lock, contract.as_ref())
}
LockMode::Net => conv.convert_order_request(&req, ConvertMode::Net, contract.as_ref()),
LockMode::Auto => {
if req.offset != Offset::CLOSE {
return conv.convert_order_request(&req, ConvertMode::Auto, contract.as_ref());
}
let Some(c) = contract else { return vec![req] };
conv.convert_order_request(&req, ConvertMode::Auto, Some(&c))
}
}
}
fn unknown_service_probe(&self) -> Result<Vec<OrderData>, AskError> {
self.ctx
.get()
.unwrap()
.ask_service::<GetAllOrdersReq, Vec<OrderData>>(
"offset.nothing",
GetAllOrdersReq,
Duration::from_millis(200),
)
}
}
impl Plugin for OffsetFixture {
fn name(&self) -> &str {
FIXTURE_NAME
}
fn deps(&self) -> &[&str] {
&["oms"]
}
fn routes(&self) -> Vec<Cow<'static, str>> {
vec![
Cow::Borrowed(W_ORDER),
Cow::Borrowed(W_TRADE),
Cow::Borrowed(W_POSITION),
Cow::Borrowed(W_CONTRACT),
Cow::Borrowed(W_POS_DONE),
Cow::Owned(e_req_type(self.name())),
]
}
fn version(&self) -> Cow<'static, str> {
Cow::Borrowed(env!("CARGO_PKG_VERSION"))
}
fn services(&self) -> Vec<ServiceDesc> {
vec![
ServiceDesc::describe::<GetHoldingReq, Option<PositionHolding>>(
"offset.get_holding",
"",
),
ServiceDesc::describe::<ConvertReq, Vec<OrderRequest>>("offset.convert", ""),
]
}
fn on_init(&self, ctx: &PluginContext) {
let _ = self.ctx.set(ctx.clone());
}
fn on_mail(&self, event: &Event) {
match event.event_type.as_str() {
W_ORDER => {
if let Some(o) = event.data.downcast_ref::<OrderData>() {
self.on_order(o);
}
}
W_TRADE => {
if let Some(t) = event.data.downcast_ref::<TradeData>() {
self.on_trade(t);
}
}
W_POSITION => {
if let Some(p) = event.data.downcast_ref::<PositionData>() {
self.on_position(p);
}
}
W_CONTRACT => {
if let Some(c) = event.data.downcast_ref::<ContractData>() {
self.on_contract(c);
}
}
W_POS_DONE => {
if let Some(g) = event.data.downcast_ref::<String>() {
self.on_pos_done(g);
}
}
t if t == e_req_type(self.name()) => self.on_request(event),
_ => {}
}
}
}
impl OffsetFixture {
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!(
GetHoldingReq,
Option<PositionHolding>,
|r: &GetHoldingReq| self.holding(r.gateway_name.as_str(), r.en_symbol.as_str())
);
arm!(ConvertReq, Vec<OrderRequest>, |r: &ConvertReq| self
.convert(r.req.clone(), r.gateway_name.as_str(), r.lock_mode));
let _ = env
.reply
.send(Box::new(Err::<(), ReplyErr>(ReplyErr::InvalidPayload)));
}
}
fn feq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
fn contract(gateway: &str, symbol: &str, ex: Exchange, product: &str) -> ContractData {
ContractData {
gateway_name: gateway.to_string(),
symbol: symbol.to_string(),
exchange: ex,
name: symbol.to_string(),
asset: Asset::FUTURES,
product_id: Some(product.to_string()),
..ContractData::default()
}
}
#[allow(clippy::too_many_arguments)]
fn order(
oid: &str,
status: Status,
gateway: &str,
symbol: &str,
ex: Exchange,
dir: Direction,
offset: Offset,
vol: f64,
traded: f64,
) -> OrderData {
OrderData {
gateway_name: gateway.to_string(),
symbol: symbol.to_string(),
exchange: ex,
orderid: oid.to_string(),
order_type: OrderType::LIMIT,
direction: Some(dir),
offset,
price: 3500.0,
volume: vol,
traded,
status,
..OrderData::default()
}
}
fn trade(
gateway: &str,
symbol: &str,
ex: Exchange,
dir: Direction,
offset: Offset,
vol: f64,
) -> TradeData {
TradeData {
gateway_name: gateway.to_string(),
symbol: symbol.to_string(),
exchange: ex,
orderid: "1".to_string(),
tradeid: "t1".to_string(),
direction: Some(dir),
offset,
price: 3500.0,
volume: vol,
..TradeData::default()
}
}
fn position(
gateway: &str,
symbol: &str,
ex: Exchange,
dir: Direction,
vol: f64,
yd: f64,
) -> PositionData {
PositionData {
gateway_name: gateway.to_string(),
symbol: symbol.to_string(),
exchange: ex,
direction: dir,
volume: vol,
yd_volume: yd,
..PositionData::default()
}
}
fn req(symbol: &str, ex: Exchange, dir: Direction, offset: Offset, vol: f64) -> OrderRequest {
OrderRequest {
symbol: symbol.to_string(),
exchange: ex,
direction: dir,
order_type: OrderType::LIMIT,
volume: vol,
price: 3500.0,
offset,
reference: String::new(),
old_order_id: String::new(),
}
}
fn bus_ask<T: Any + Send + Sync, R: Any + Send + Sync>(
ev: &EventEngine,
target: &str,
req: T,
) -> Result<R, ReplyErr> {
let (env, rx) = Envelope::new(Arc::new(req));
ev.put(Event::with_data(e_req_type(target), Arc::new(env)))
.expect("bus alive");
let boxed = rx
.recv_timeout(Duration::from_secs(2))
.expect("reply within 2s (serial mailbox makes put→ask deterministic)");
match boxed.downcast::<Result<R, ReplyErr>>() {
Ok(inner) => *inner,
Err(_) => panic!("reply contract mismatch"),
}
}
fn put<T: Any + Send + Sync>(ev: &EventEngine, ty: &str, payload: T) {
ev.put(Event::with_data(ty.to_string(), Arc::new(payload)))
.expect("bus alive");
}
fn wait_for(ms: u64, mut f: impl FnMut() -> bool) -> bool {
let t0 = Instant::now();
loop {
if f() {
return true;
}
if t0.elapsed() >= Duration::from_millis(ms) {
return f();
}
std::thread::sleep(Duration::from_millis(2));
}
}
struct Rig {
host: MainEngine,
}
fn rig() -> (Rig, Arc<EventEngine>, Arc<OmsPlugin>, Arc<OffsetFixture>) {
let ev = Arc::new(EventEngine::new());
let host = MainEngine::new(Some(ev.clone()));
host.add_plugin(Arc::new(ek_cfg::CfgPlugin::new(
ek_cfg::Store::with_root(std::path::PathBuf::from("target/asm-cfg-rig")),
false,
)))
.expect("cfg add");
let oms = Arc::new(OmsPlugin::new());
host.add_plugin(oms.clone()).expect("oms add");
let fx = Arc::new(OffsetFixture::new());
(Rig { host }, ev, oms, fx)
}
fn close_all(host: &MainEngine) {
host.close_all(Duration::from_secs(2));
}
#[test]
fn a1_topo_assembly() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
let seen: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let cap = seen.clone();
ev.register_general(Arc::new(move |e: &Event| {
if e.event_type == EVENT_PLUG_UP {
if let Some(n) = e.data.downcast_ref::<PlugNotice>() {
cap.lock().unwrap().push(n.name.clone());
}
}
}));
rig.host.start().expect("start");
assert!(
wait_for(2000, || *seen.lock().unwrap()
== vec![
"cfg".to_string(),
"oms".to_string(),
FIXTURE_NAME.to_string()
]),
"ePlugUp order must be cfg → oms → offset (deps topo,步骤 2 口径)"
);
assert_eq!(rig.host.get_state("cfg"), Some(PluginState::Running));
assert_eq!(rig.host.get_state("oms"), Some(PluginState::Running));
assert_eq!(rig.host.get_state(FIXTURE_NAME), Some(PluginState::Running));
assert_eq!(
rig.host.plugin_names(),
vec!["cfg".to_string(), "oms".to_string(), "offset".to_string()]
);
close_all(&rig.host);
}
fn host_add(rig: &Rig, fx: &Arc<OffsetFixture>) {
rig.host.add_plugin(fx.clone()).expect("fixture add");
}
#[test]
fn a2_missing_dep() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let ev = Arc::new(EventEngine::new());
let host = MainEngine::new(Some(ev));
host.add_plugin(Arc::new(OffsetFixture::new()))
.expect("fixture add");
let err = host.start().expect_err("dangling deps must fail at start");
assert!(matches!(err, PluginError::UnknownDep(_)), "got: {err:?}");
}
#[test]
fn a3_duplicate_name() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let ev = Arc::new(EventEngine::new());
let host = MainEngine::new(Some(ev));
host.add_plugin(Arc::new(OmsPlugin::new())).expect("first");
let err = host
.add_plugin(Arc::new(OmsPlugin::new()))
.expect_err("dup");
assert!(matches!(err, PluginError::Duplicate(_)), "got: {err:?}");
}
#[test]
fn b1_contract_over_bus() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, _fx) = rig();
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "rb2601", Exchange::SHFE, "rb"),
);
let hit: Option<ContractData> = bus_ask(
&ev,
"oms",
GetContractReq {
en_symbol: "SHFE.rb2601".into(),
},
)
.expect("ok");
assert_eq!(hit.expect("cached").symbol, "rb2601");
let miss: Option<ContractData> = bus_ask(
&ev,
"oms",
GetContractReq {
en_symbol: "SHFE.nope".into(),
},
)
.expect("ok");
assert!(miss.is_none());
close_all(&rig.host);
}
#[test]
fn b2_positions_and_active_orders() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, _fx) = rig();
rig.host.start().expect("start");
let p = position("gx", "IF2506", Exchange::CFFEX, Direction::LONG, 10.0, 4.0);
put(&ev, W_POSITION, p.clone());
put(
&ev,
W_ORDER,
order(
"7",
Status::NOTTRADED,
"gx",
"IF2506",
Exchange::CFFEX,
Direction::SHORT,
Offset::CLOSE,
4.0,
0.0,
),
);
let got: Option<PositionData> = bus_ask(
&ev,
"oms",
GetPositionReq {
en_positionid: p.en_positionid(),
},
)
.expect("ok");
let got = got.expect("cached");
assert!(feq(got.volume, 10.0) && feq(got.yd_volume, 4.0));
let active: Vec<OrderData> = bus_ask(
&ev,
"oms",
GetAllActiveOrdersReq {
en_symbol: Some("CFFEX.IF2506".into()),
},
)
.expect("ok");
assert_eq!(active.len(), 1);
assert!(feq(active[0].volume, 4.0));
close_all(&rig.host);
}
#[test]
fn b4_counters_over_bus() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, _fx) = rig();
rig.host.start().expect("start");
put(
&ev,
W_ORDER,
order(
"9",
Status::NOTTRADED,
"gx",
"IF2506",
Exchange::CFFEX,
Direction::SHORT,
Offset::CLOSE,
5.0,
0.0,
),
);
put(
&ev,
W_ORDER,
order(
"9",
Status::CANCELLED,
"gx",
"IF2506",
Exchange::CFFEX,
Direction::SHORT,
Offset::CLOSE,
5.0,
0.0,
),
);
let contract_key = "CFFEX.IF2506";
let ti: Option<u64> = bus_ask(
&ev,
"oms",
TradeInfoCountReq {
gateway_name: "gx".into(),
en_symbol: contract_key.into(),
},
)
.expect("ok");
assert_eq!(ti, Some(2), "首见 +1、撤单 +1");
let cc: Option<u64> = bus_ask(
&ev,
"oms",
CancelCountReq {
gateway_name: "gx".into(),
en_symbol: contract_key.into(),
},
)
.expect("ok");
assert_eq!(cc, Some(1));
let oc: Option<u64> = bus_ask(
&ev,
"oms",
OrderCancelCountReq {
en_orderid: "gx.9".into(),
},
)
.expect("ok");
assert_eq!(oc, Some(1));
close_all(&rig.host);
}
fn c1_cfg(host: &MainEngine) {
host.add_plugin(Arc::new(ek_cfg::CfgPlugin::new(
ek_cfg::Store::with_root(std::path::PathBuf::from("target/asm-cfg-rig")),
false,
)))
.expect("cfg add");
}
#[test]
fn c1_dynamic_add_backquery() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let ev = Arc::new(EventEngine::new());
let host = MainEngine::new(Some(ev.clone()));
c1_cfg(&host);
let oms = Arc::new(OmsPlugin::new());
host.add_plugin(oms).expect("oms add");
host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "rb2601", Exchange::SHFE, "rb"),
);
let cached: Option<ContractData> = bus_ask(
&ev,
"oms",
GetContractReq {
en_symbol: "SHFE.rb2601".into(),
},
)
.expect("ok");
assert!(
cached.is_some(),
"oms must have cached the contract before fixture joins"
);
let fx = Arc::new(OffsetFixture::new());
host.add_plugin(fx.clone()).expect("dynamic add launches");
assert_eq!(host.get_state(FIXTURE_NAME), Some(PluginState::Running));
let t1 = trade(
"gx",
"rb2601",
Exchange::SHFE,
Direction::LONG,
Offset::OPEN,
2.0,
);
put(&ev, W_TRADE, t1);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "SHFE.rb2601".into(),
},
)
.expect("ok");
let h = h.expect("backquery must build the ledger");
assert_eq!(h.en_symbol, "SHFE.rb2601");
assert!(feq(h.long_td, 2.0), "trade ingested after backquery");
assert_eq!(fx.asks_made(), 1, "exactly one backquery");
close_all(&host);
}
#[test]
fn c2_local_cache_path() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "IF2506", Exchange::CFFEX, "IF"),
);
let t1 = trade(
"gx",
"IF2506",
Exchange::CFFEX,
Direction::LONG,
Offset::OPEN,
3.0,
);
put(&ev, W_TRADE, t1);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "CFFEX.IF2506".into(),
},
)
.expect("ok");
assert!(feq(h.unwrap().long_td, 3.0));
assert_eq!(fx.asks_made(), 0, "contract was broadcast; zero asks");
close_all(&rig.host);
}
#[test]
fn c3_no_contract_gate() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_TRADE,
trade(
"gx",
"cu2601",
Exchange::SHFE,
Direction::LONG,
Offset::OPEN,
1.0,
),
);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "SHFE.cu2601".into(),
},
)
.expect("ok");
assert!(h.is_none(), "gate must keep the empty ledger absent");
assert_eq!(fx.asks_made(), 1, "one backquery came up empty");
close_all(&rig.host);
}
#[test]
fn c4_holding_service() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "IF2506", Exchange::CFFEX, "IF"),
);
put(
&ev,
W_POSITION,
position("gx", "IF2506", Exchange::CFFEX, Direction::LONG, 10.0, 4.0),
);
put(
&ev,
W_ORDER,
order(
"7",
Status::NOTTRADED,
"gx",
"IF2506",
Exchange::CFFEX,
Direction::SHORT,
Offset::CLOSE,
4.0,
0.0,
),
);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "CFFEX.IF2506".into(),
},
)
.expect("ok");
let h = h.expect("ledger present");
assert_eq!(h.en_symbol, "CFFEX.IF2506");
assert_eq!(h.exchange, Exchange::CFFEX);
assert!(feq(h.long_pos, 10.0) && feq(h.long_yd, 4.0) && feq(h.long_td, 6.0));
assert!(feq(h.long_td_frozen, 4.0) && feq(h.long_yd_frozen, 0.0));
assert!(feq(h.long_pos_frozen, 4.0));
assert!(feq(h.short_pos, 0.0) && feq(h.short_pos_frozen, 0.0));
close_all(&rig.host);
}
#[test]
fn d1_shfe_split_via_service() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "rb2601", Exchange::SHFE, "rb"),
);
put(
&ev,
W_POSITION,
position("gx", "rb2601", Exchange::SHFE, Direction::LONG, 5.0, 2.0),
);
let out: Vec<OrderRequest> = bus_ask(
&ev,
FIXTURE_NAME,
ConvertReq {
req: req(
"rb2601",
Exchange::SHFE,
Direction::SHORT,
Offset::CLOSE,
5.0,
),
gateway_name: "gx".into(),
lock_mode: LockMode::Auto,
},
)
.expect("ok");
assert_eq!(out.len(), 2, "td first(yd leg then follows");
assert_eq!(out[0].offset, Offset::CLOSETODAY);
assert!(feq(out[0].volume, 3.0));
assert_eq!(out[1].offset, Offset::CLOSEYESTERDAY);
assert!(feq(out[1].volume, 2.0));
close_all(&rig.host);
}
#[test]
fn d2_reject_insufficient() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "rb2601", Exchange::SHFE, "rb"),
);
put(
&ev,
W_POSITION,
position("gx", "rb2601", Exchange::SHFE, Direction::LONG, 5.0, 2.0),
);
let out: Vec<OrderRequest> = bus_ask(
&ev,
FIXTURE_NAME,
ConvertReq {
req: req(
"rb2601",
Exchange::SHFE,
Direction::SHORT,
Offset::CLOSE,
6.0,
),
gateway_name: "gx".into(),
lock_mode: LockMode::Auto,
},
)
.expect("ok");
assert!(out.is_empty(), "insufficient closeable must reject");
close_all(&rig.host);
}
#[test]
fn e1_unknown_service() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, _ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
let err = fx.unknown_service_probe().expect_err("no provider");
assert!(
matches!(err, AskError::UnknownService { .. }),
"got: {err:?}"
);
close_all(&rig.host);
}
#[test]
fn e2_close_terminal_state() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
close_all(&rig.host);
assert!(rig.host.plugin_names().is_empty(), "registry cleared");
assert_eq!(rig.host.get_state("oms"), None);
assert!(!ev.is_active(), "bus stopped by close_all");
}
#[test]
fn f1_phase_gate() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-station/target/cfg-test-root"));
let (rig, ev, _oms, fx) = rig();
host_add(&rig, &fx);
rig.host.start().expect("start");
put(
&ev,
W_CONTRACT,
contract("gx", "IF2506", Exchange::CFFEX, "IF"),
);
put(
&ev,
W_POSITION,
position("gx", "IF2506", Exchange::CFFEX, Direction::LONG, 2.0, 0.0),
);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "CFFEX.IF2506".into(),
},
)
.expect("ok");
assert!(feq(h.unwrap().long_pos, 2.0), "init-phase ingests");
put(&ev, W_POS_DONE, "gx".to_string());
put(
&ev,
W_POSITION,
position("gx", "IF2506", Exchange::CFFEX, Direction::LONG, 7.0, 0.0),
);
let h: Option<PositionHolding> = bus_ask(
&ev,
FIXTURE_NAME,
GetHoldingReq {
gateway_name: "gx".into(),
en_symbol: "CFFEX.IF2506".into(),
},
)
.expect("ok");
assert!(
feq(h.unwrap().long_pos, 2.0),
"run-phase must NOT overwrite the ledger"
);
let p = position("gx", "IF2506", Exchange::CFFEX, Direction::LONG, 7.0, 0.0);
let got: Option<PositionData> = bus_ask(
&ev,
"oms",
GetPositionReq {
en_positionid: p.en_positionid(),
},
)
.expect("ok");
assert!(feq(got.expect("cached").volume, 7.0));
close_all(&rig.host);
}