1use std::io::{self, Read, Write};
15use std::ops::{Deref, DerefMut};
16
17use melin_app::{Application, ApplyCtx, RejectReason as TransportRejectReason};
18use melin_exchange_core::exchange::Exchange;
19use melin_exchange_core::snapshot as engine_snapshot;
20use melin_trading::trading_event::TradingEvent;
21use melin_types::types::{
22 AccountId, ExecutionReport, OrderId, QueryResponse, RejectReason as EngineRejectReason, Symbol,
23};
24
25const _: () = assert!(size_of::<melin_transport_core::pipeline::InputSlot<TradingEvent>>() == 128);
39#[cfg(not(feature = "latency-trace"))]
45const _: () = assert!(
46 size_of::<melin_transport_core::pipeline::OutputSlot<ExecutionReport, QueryResponse>>() == 424
47);
48const _: () = assert!(size_of::<melin_journal::JournalEvent<TradingEvent>>() == 64);
49const _: () = assert!(size_of::<ExecutionReport>() == 64);
50
51pub struct ServerApp(pub Exchange);
62
63impl ServerApp {
64 pub fn new() -> Self {
68 ServerApp(Exchange::new())
69 }
70}
71
72impl Default for ServerApp {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl Deref for ServerApp {
79 type Target = Exchange;
80
81 #[inline]
82 fn deref(&self) -> &Exchange {
83 &self.0
84 }
85}
86
87impl DerefMut for ServerApp {
88 #[inline]
89 fn deref_mut(&mut self) -> &mut Exchange {
90 &mut self.0
91 }
92}
93
94impl Application for ServerApp {
95 type Event = TradingEvent;
96 type Report = ExecutionReport;
97 type QueryResponse = QueryResponse;
98
99 const APP_VERSION: u16 = engine_snapshot::PAYLOAD_VERSION;
103
104 #[inline]
109 fn apply(
110 &mut self,
111 event: Self::Event,
112 ctx: &ApplyCtx,
113 out: &mut Vec<Self::Report>,
114 ) -> Option<Self::QueryResponse> {
115 self.0.set_current_event_ts_ns(ctx.now_ns);
121 match event {
122 TradingEvent::AddInstrument { spec } => {
123 self.0.add_instrument(spec);
124 None
125 }
126 TradingEvent::Deposit {
127 account,
128 currency,
129 amount,
130 } => {
131 self.0.deposit(account, currency, amount);
132 None
133 }
134 TradingEvent::SubmitOrder { symbol, order } => {
135 self.0.execute(symbol, order, out);
136 None
137 }
138 TradingEvent::CancelOrder {
139 symbol,
140 account,
141 order_id,
142 } => {
143 self.0.cancel(symbol, account, order_id, out);
144 None
145 }
146 TradingEvent::SetRiskLimits { symbol, limits } => {
147 self.0.set_risk_limits(symbol, limits);
148 None
149 }
150 TradingEvent::CancelAll { account } => {
151 self.0.cancel_all(account, out);
152 None
153 }
154 TradingEvent::SetCircuitBreaker { symbol, config } => {
155 self.0.set_circuit_breaker(symbol, config);
156 None
157 }
158 TradingEvent::CancelReplace {
159 symbol,
160 account,
161 order_id,
162 new_price,
163 new_quantity,
164 } => {
165 self.0
166 .cancel_replace(symbol, account, order_id, new_price, new_quantity, out);
167 None
168 }
169 TradingEvent::SetFeeSchedule { symbol, schedule } => {
170 self.0.set_fee_schedule(symbol, schedule, out);
171 None
172 }
173 TradingEvent::ProvisionAccount { account, amount } => {
174 self.0.provision_account(account, amount);
175 None
176 }
177 TradingEvent::Withdraw {
178 account,
179 currency,
180 amount,
181 } => {
182 if let Err(reason) = self.0.withdraw(account, currency, amount) {
183 out.push(ExecutionReport::Rejected {
192 order_id: OrderId(0),
193 symbol: Symbol(0),
194 account,
195 reason,
196 });
197 }
198 None
199 }
200 TradingEvent::EndOfDay => {
201 self.0.end_of_day(out);
202 None
203 }
204 TradingEvent::DisableInstrument { symbol } => {
205 self.0.disable_instrument(symbol, out);
206 None
207 }
208 TradingEvent::EnableInstrument { symbol } => {
209 self.0.enable_instrument(symbol, out);
210 None
211 }
212 TradingEvent::RemoveInstrument { symbol } => {
213 self.0.remove_instrument(symbol, out);
214 None
215 }
216 TradingEvent::QueryStats => {
217 Some(QueryResponse::Stats {
221 active_connections: ctx.active_connections,
222 events_processed: ctx.events_processed,
223 journal_sequence: ctx.journal_sequence,
224 })
225 }
226 TradingEvent::QueryPosition { account } => {
227 let (balances, count) = self.0.accounts().balances_for(account);
228 Some(QueryResponse::Position {
229 account,
230 balances,
231 count,
232 })
233 }
234 TradingEvent::QueryRequestSeq => {
235 Some(QueryResponse::RequestSeqHwm {
240 hwm: self.0.request_seq_hwm(ctx.key_hash),
241 })
242 }
243 }
244 }
245
246 #[inline]
247 fn tick(&mut self, now_ns: u64, out: &mut Vec<Self::Report>) {
248 self.0.drain_due_scheduled_tasks(now_ns, out);
249 }
250
251 #[inline]
252 fn check_request_seq(&mut self, key_hash: u64, seq: u64) -> bool {
253 Exchange::check_request_seq(&mut self.0, key_hash, seq)
254 }
255
256 fn prefault(&mut self) {
261 Exchange::prefault(&mut self.0);
262 }
263
264 fn clone_via_snapshot(&self) -> std::io::Result<Self> {
269 Ok(ServerApp(Exchange::clone_via_snapshot(&self.0)))
270 }
271
272 fn build_reject(event: &Self::Event, reason: TransportRejectReason) -> Self::Report {
273 let engine_reason = match reason {
274 TransportRejectReason::DuplicateRequest => EngineRejectReason::DuplicateRequest,
275 TransportRejectReason::ReplicaDisconnected => EngineRejectReason::ReplicaDisconnected,
276 };
277 ExecutionReport::Rejected {
278 order_id: extract_order_id(event),
279 symbol: extract_symbol(event),
280 account: extract_account_id(event),
281 reason: engine_reason,
282 }
283 }
284
285 fn snapshot<W: Write>(&self, w: &mut W) -> io::Result<()> {
292 let bytes = engine_snapshot::encode_exchange_payload(&self.0);
293 w.write_all(&bytes)
294 }
295
296 fn restore<R: Read>(r: &mut R) -> io::Result<Self> {
297 let mut bytes = Vec::new();
298 r.read_to_end(&mut bytes)?;
299 engine_snapshot::decode_exchange_payload(&bytes)
300 .map(ServerApp)
301 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
302 }
303}
304
305fn extract_order_id(event: &TradingEvent) -> OrderId {
309 match event {
310 TradingEvent::SubmitOrder { order, .. } => order.id,
311 TradingEvent::CancelOrder { order_id, .. }
312 | TradingEvent::CancelReplace { order_id, .. } => *order_id,
313 _ => OrderId(0),
314 }
315}
316
317fn extract_account_id(event: &TradingEvent) -> AccountId {
318 match event {
319 TradingEvent::SubmitOrder { order, .. } => order.account,
320 TradingEvent::CancelOrder { account, .. }
321 | TradingEvent::CancelAll { account }
322 | TradingEvent::CancelReplace { account, .. }
323 | TradingEvent::Deposit { account, .. }
324 | TradingEvent::Withdraw { account, .. }
325 | TradingEvent::ProvisionAccount { account, .. }
326 | TradingEvent::QueryPosition { account } => *account,
327 _ => AccountId(0),
328 }
329}
330
331fn extract_symbol(event: &TradingEvent) -> Symbol {
332 match event {
333 TradingEvent::SubmitOrder { symbol, .. }
334 | TradingEvent::CancelOrder { symbol, .. }
335 | TradingEvent::CancelReplace { symbol, .. }
336 | TradingEvent::SetRiskLimits { symbol, .. }
337 | TradingEvent::SetCircuitBreaker { symbol, .. }
338 | TradingEvent::SetFeeSchedule { symbol, .. }
339 | TradingEvent::DisableInstrument { symbol }
340 | TradingEvent::EnableInstrument { symbol }
341 | TradingEvent::RemoveInstrument { symbol } => *symbol,
342 _ => Symbol(0),
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 use std::io::Cursor;
351 use std::num::NonZeroU64;
352
353 use melin_types::types::{
354 CurrencyId, InstrumentSpec, Order, OrderType, Price, Quantity, SelfTradeProtection, Side,
355 TimeInForce,
356 };
357
358 fn price(p: u64) -> Price {
359 Price(NonZeroU64::new(p).unwrap())
360 }
361 fn qty(q: u64) -> Quantity {
362 Quantity(NonZeroU64::new(q).unwrap())
363 }
364
365 fn seeded_app() -> ServerApp {
368 let mut ex = Exchange::new();
369 ex.add_instrument(InstrumentSpec {
370 symbol: Symbol(1),
371 base: CurrencyId(1),
372 quote: CurrencyId(2),
373 });
374 ex.deposit(AccountId(1), CurrencyId(2), 1_000_000);
375 ServerApp(ex)
376 }
377
378 #[test]
379 fn apply_submit_order_produces_placed_report() {
380 let mut app = seeded_app();
381 let mut reports = Vec::new();
382 let ctx = ApplyCtx {
383 now_ns: 0,
384 journal_sequence: 0,
385 active_connections: 0,
386 events_processed: 0,
387 key_hash: 0,
388 };
389 let ev = TradingEvent::SubmitOrder {
390 symbol: Symbol(1),
391 order: Order {
392 id: OrderId(1),
393 account: AccountId(1),
394 side: Side::Buy,
395 order_type: OrderType::Limit {
396 price: price(100),
397 post_only: false,
398 },
399 quantity: qty(10),
400 time_in_force: TimeInForce::GTC,
401 stp: SelfTradeProtection::Allow,
402 expiry_ns: 0,
403 },
404 };
405 <ServerApp as Application>::apply(&mut app, ev, &ctx, &mut reports);
406 assert!(
407 !reports.is_empty(),
408 "apply should emit at least one report for a resting order"
409 );
410 }
411
412 #[test]
413 fn tick_advances_scheduler_clock() {
414 let mut app = ServerApp(Exchange::new());
418 let mut reports = Vec::new();
419 <ServerApp as Application>::tick(&mut app, 1_000_000_000, &mut reports);
420 assert!(reports.is_empty());
421 }
422
423 #[test]
424 fn apply_query_request_seq_returns_per_key_hwm() {
425 let mut app = seeded_app();
426
427 let key_a: u64 = 0xAAAA_AAAA_AAAA_AAAA;
430 let key_b: u64 = 0xBBBB_BBBB_BBBB_BBBB;
431 for seq in 1..=7 {
432 assert!(<ServerApp as Application>::check_request_seq(
433 &mut app, key_a, seq
434 ));
435 }
436 for seq in 1..=3 {
437 assert!(<ServerApp as Application>::check_request_seq(
438 &mut app, key_b, seq
439 ));
440 }
441
442 let mut reports = Vec::new();
443 let mk_ctx = |kh| ApplyCtx {
444 now_ns: 0,
445 journal_sequence: 0,
446 active_connections: 0,
447 events_processed: 0,
448 key_hash: kh,
449 };
450
451 let resp_a = <ServerApp as Application>::apply(
454 &mut app,
455 TradingEvent::QueryRequestSeq,
456 &mk_ctx(key_a),
457 &mut reports,
458 );
459 assert_eq!(resp_a, Some(QueryResponse::RequestSeqHwm { hwm: 7 }));
460
461 let resp_b = <ServerApp as Application>::apply(
462 &mut app,
463 TradingEvent::QueryRequestSeq,
464 &mk_ctx(key_b),
465 &mut reports,
466 );
467 assert_eq!(resp_b, Some(QueryResponse::RequestSeqHwm { hwm: 3 }));
468
469 let resp_unknown = <ServerApp as Application>::apply(
471 &mut app,
472 TradingEvent::QueryRequestSeq,
473 &mk_ctx(0xDEAD_BEEF),
474 &mut reports,
475 );
476 assert_eq!(resp_unknown, Some(QueryResponse::RequestSeqHwm { hwm: 0 }));
477
478 assert_eq!(app.0.request_seq_hwm(key_a), 7);
480 assert_eq!(app.0.request_seq_hwm(key_b), 3);
481 }
482
483 #[test]
484 fn check_request_seq_rejects_duplicates() {
485 let mut app = ServerApp(Exchange::new());
486 assert!(<ServerApp as Application>::check_request_seq(
487 &mut app, 42, 1
488 ));
489 assert!(<ServerApp as Application>::check_request_seq(
490 &mut app, 42, 2
491 ));
492 assert!(!<ServerApp as Application>::check_request_seq(
493 &mut app, 42, 2
494 ));
495 assert!(!<ServerApp as Application>::check_request_seq(
496 &mut app, 42, 1
497 ));
498 }
499
500 #[test]
501 fn build_reject_maps_transport_reasons() {
502 let ev = TradingEvent::SubmitOrder {
503 symbol: Symbol(7),
504 order: Order {
505 id: OrderId(42),
506 account: AccountId(3),
507 side: Side::Buy,
508 order_type: OrderType::Market,
509 quantity: qty(1),
510 time_in_force: TimeInForce::IOC,
511 stp: SelfTradeProtection::Allow,
512 expiry_ns: 0,
513 },
514 };
515 let r =
516 <ServerApp as Application>::build_reject(&ev, TransportRejectReason::DuplicateRequest);
517 match r {
518 ExecutionReport::Rejected {
519 order_id,
520 symbol,
521 account,
522 reason,
523 } => {
524 assert_eq!(order_id, OrderId(42));
525 assert_eq!(symbol, Symbol(7));
526 assert_eq!(account, AccountId(3));
527 assert_eq!(reason, EngineRejectReason::DuplicateRequest);
528 }
529 other => panic!("expected Rejected, got {other:?}"),
530 }
531
532 let r = <ServerApp as Application>::build_reject(
533 &TradingEvent::CancelAll {
534 account: AccountId(9),
535 },
536 TransportRejectReason::ReplicaDisconnected,
537 );
538 match r {
539 ExecutionReport::Rejected {
540 order_id,
541 symbol,
542 account,
543 reason,
544 } => {
545 assert_eq!(order_id, OrderId(0));
546 assert_eq!(symbol, Symbol(0));
547 assert_eq!(account, AccountId(9));
548 assert_eq!(reason, EngineRejectReason::ReplicaDisconnected);
549 }
550 other => panic!("expected Rejected, got {other:?}"),
551 }
552 }
553
554 #[test]
555 fn apply_withdraw_emits_rejection_on_failure() {
556 let mut app = seeded_app();
557 let ctx = ApplyCtx {
558 now_ns: 0,
559 journal_sequence: 0,
560 active_connections: 0,
561 events_processed: 0,
562 key_hash: 0,
563 };
564
565 let mut reports = Vec::new();
568 <ServerApp as Application>::apply(
569 &mut app,
570 TradingEvent::Withdraw {
571 account: AccountId(1),
572 currency: CurrencyId(2),
573 amount: 2_000_000,
574 },
575 &ctx,
576 &mut reports,
577 );
578 assert_eq!(reports.len(), 1);
579 match reports[0] {
580 ExecutionReport::Rejected {
581 order_id,
582 symbol,
583 account,
584 reason,
585 } => {
586 assert_eq!(order_id, OrderId(0));
587 assert_eq!(symbol, Symbol(0));
588 assert_eq!(account, AccountId(1));
589 assert_eq!(reason, EngineRejectReason::InsufficientBalance);
590 }
591 ref other => panic!("expected Rejected, got {other:?}"),
592 }
593
594 let mut reports = Vec::new();
597 <ServerApp as Application>::apply(
598 &mut app,
599 TradingEvent::Withdraw {
600 account: AccountId(999),
601 currency: CurrencyId(2),
602 amount: 1,
603 },
604 &ctx,
605 &mut reports,
606 );
607 assert_eq!(reports.len(), 1);
608 match reports[0] {
609 ExecutionReport::Rejected {
610 reason, account, ..
611 } => {
612 assert_eq!(account, AccountId(999));
613 assert_eq!(reason, EngineRejectReason::UnknownAccount);
614 }
615 ref other => panic!("expected Rejected, got {other:?}"),
616 }
617
618 let mut placed = Vec::new();
620 <ServerApp as Application>::apply(
621 &mut app,
622 TradingEvent::SubmitOrder {
623 symbol: Symbol(1),
624 order: Order {
625 id: OrderId(1),
626 account: AccountId(1),
627 side: Side::Buy,
628 order_type: OrderType::Limit {
629 price: price(100),
630 post_only: false,
631 },
632 quantity: qty(10),
633 time_in_force: TimeInForce::GTC,
634 stp: SelfTradeProtection::Allow,
635 expiry_ns: 0,
636 },
637 },
638 &ctx,
639 &mut placed,
640 );
641
642 let mut reports = Vec::new();
643 <ServerApp as Application>::apply(
644 &mut app,
645 TradingEvent::Withdraw {
646 account: AccountId(1),
647 currency: CurrencyId(2),
648 amount: 1,
649 },
650 &ctx,
651 &mut reports,
652 );
653 assert_eq!(reports.len(), 1);
654 match reports[0] {
655 ExecutionReport::Rejected {
656 reason, account, ..
657 } => {
658 assert_eq!(account, AccountId(1));
659 assert_eq!(reason, EngineRejectReason::HasRestingOrders);
660 }
661 ref other => panic!("expected Rejected, got {other:?}"),
662 }
663
664 let mut reports = Vec::new();
666 let mut clean = ServerApp::new();
667 clean.0.deposit(AccountId(7), CurrencyId(2), 500);
668 <ServerApp as Application>::apply(
669 &mut clean,
670 TradingEvent::Withdraw {
671 account: AccountId(7),
672 currency: CurrencyId(2),
673 amount: 200,
674 },
675 &ctx,
676 &mut reports,
677 );
678 assert!(
679 reports.is_empty(),
680 "successful withdraw must not emit reports"
681 );
682 }
683
684 #[test]
685 fn snapshot_restore_round_trip_preserves_state() {
686 let mut before = seeded_app();
687 let mut reports = Vec::new();
688 before.0.execute(
691 Symbol(1),
692 Order {
693 id: OrderId(1),
694 account: AccountId(1),
695 side: Side::Buy,
696 order_type: OrderType::Limit {
697 price: price(100),
698 post_only: false,
699 },
700 quantity: qty(10),
701 time_in_force: TimeInForce::GTC,
702 stp: SelfTradeProtection::Allow,
703 expiry_ns: 0,
704 },
705 &mut reports,
706 );
707 let reports_before = reports.clone();
708
709 let mut buf = Vec::new();
710 <ServerApp as Application>::snapshot(&before, &mut buf).expect("snapshot");
711
712 let mut cursor = Cursor::new(buf);
713 let mut after = <ServerApp as Application>::restore(&mut cursor).expect("restore");
714
715 let mut reports_after = reports_before.clone();
719 reports_after.clear();
720 after.0.execute(
721 Symbol(1),
722 Order {
723 id: OrderId(2),
724 account: AccountId(1),
725 side: Side::Buy,
726 order_type: OrderType::Limit {
727 price: price(99),
728 post_only: false,
729 },
730 quantity: qty(5),
731 time_in_force: TimeInForce::GTC,
732 stp: SelfTradeProtection::Allow,
733 expiry_ns: 0,
734 },
735 &mut reports_after,
736 );
737 assert!(
738 !reports_after.is_empty(),
739 "restored exchange must accept orders"
740 );
741 }
742}