use std::collections::{HashMap, VecDeque};
use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, SyncSender, TrySendError};
use std::sync::{Arc, Mutex};
use of_execution_core::{
AccountId, AmendRequest, CancelRequest, ClientOrderId, ExecutionEvent, ExecutionSymbol,
ExecutionText, ExecutionType, FixedAscii, OrderPrice, OrderQty, OrderRequest, OrderSide,
OrderState, OrderStatus, OrderType, RiskCheck, RiskContext, RiskDecision, RiskLimits,
RiskRejectReason, RouteId, StrategyId, TimeInForce,
};
use crate::{
AllowAllRiskGate, ExecutionAdapter, ExecutionCapabilities, ExecutionCommand,
ExecutionCommandKind, ExecutionCommandReport, ExecutionEngine, ExecutionError,
ExecutionEventBuffer, ExecutionJournal, ExecutionMetrics, ExecutionResult, InMemoryJournal,
JournalCommandKind, JournalRecord, RouteConfig, RouteKey, SimExecutionAdapter,
};
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CommandId(pub u64);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct RequestId(pub FixedAscii<40>);
impl RequestId {
pub fn new(value: &str) -> Result<Self, of_execution_core::ExecutionCoreError> {
Ok(Self(FixedAscii::new(value)?))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Debug, Default)]
pub struct CommandIdGenerator {
next: AtomicU64,
}
impl CommandIdGenerator {
pub const fn new(first: u64) -> Self {
Self {
next: AtomicU64::new(first),
}
}
pub fn next(&self) -> CommandId {
CommandId(self.next.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandCorrelation {
pub command_id: CommandId,
pub request_id: RequestId,
pub client_order_id: ClientOrderId,
pub kind: ExecutionCommandKind,
}
impl CommandCorrelation {
pub const fn new(
command_id: CommandId,
request_id: RequestId,
client_order_id: ClientOrderId,
kind: ExecutionCommandKind,
) -> Self {
Self {
command_id,
request_id,
client_order_id,
kind,
}
}
}
#[derive(Debug)]
pub struct ExecutionEventSubscriber {
receiver: Receiver<ExecutionEvent>,
}
impl ExecutionEventSubscriber {
pub fn recv(&self) -> Result<ExecutionEvent, ExecutionError> {
self.receiver
.recv()
.map_err(|_| ExecutionError::Adapter("execution event fanout closed".to_string()))
}
pub fn try_recv(&self) -> Option<ExecutionEvent> {
self.receiver.try_recv().ok()
}
}
#[derive(Debug)]
struct FanoutInner {
subscribers: Vec<SyncSender<ExecutionEvent>>,
dropped_events: u64,
}
#[derive(Debug, Clone)]
pub struct ExecutionEventFanout {
inner: Arc<Mutex<FanoutInner>>,
subscriber_capacity: usize,
}
impl ExecutionEventFanout {
pub fn new(subscriber_capacity: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(FanoutInner {
subscribers: Vec::new(),
dropped_events: 0,
})),
subscriber_capacity,
}
}
pub fn subscribe(&self) -> ExecutionEventSubscriber {
let (tx, receiver) = mpsc::sync_channel(self.subscriber_capacity);
let mut inner = self.inner.lock().expect("fanout mutex");
inner.subscribers.push(tx);
ExecutionEventSubscriber { receiver }
}
pub fn publish(&self, event: ExecutionEvent) {
let mut inner = self.inner.lock().expect("fanout mutex");
let mut dropped = 0_u64;
inner
.subscribers
.retain(|subscriber| match subscriber.try_send(event) {
Ok(()) => true,
Err(TrySendError::Full(_)) => {
dropped = dropped.saturating_add(1);
true
}
Err(TrySendError::Disconnected(_)) => false,
});
inner.dropped_events = inner.dropped_events.saturating_add(dropped);
}
pub fn publish_buffer(&self, events: &ExecutionEventBuffer) {
for event in events.as_slice() {
self.publish(*event);
}
}
pub fn dropped_events(&self) -> u64 {
self.inner.lock().expect("fanout mutex").dropped_events
}
pub fn subscriber_count(&self) -> usize {
self.inner.lock().expect("fanout mutex").subscribers.len()
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ExecutionAdapterState {
#[default]
Disconnected = 0,
Connecting = 1,
LogonPending = 2,
Ready = 3,
Recovering = 4,
Degraded = 5,
Stopped = 6,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExecutionLifecycleSnapshot {
pub state: ExecutionAdapterState,
pub sequence: u64,
pub updated_ns: u64,
pub last_error: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ExecutionLifecycle {
snapshot: ExecutionLifecycleSnapshot,
}
impl ExecutionLifecycle {
pub fn new() -> Self {
Self::default()
}
pub fn transition(
&mut self,
state: ExecutionAdapterState,
updated_ns: u64,
last_error: Option<String>,
) -> ExecutionLifecycleSnapshot {
self.snapshot.state = state;
self.snapshot.sequence = self.snapshot.sequence.saturating_add(1);
self.snapshot.updated_ns = updated_ns;
self.snapshot.last_error = last_error;
self.snapshot.clone()
}
pub fn snapshot(&self) -> ExecutionLifecycleSnapshot {
self.snapshot.clone()
}
}
#[derive(Debug)]
pub struct FileExecutionJournal {
path: PathBuf,
file: File,
sync_on_write: bool,
}
impl FileExecutionJournal {
pub fn open(path: impl AsRef<Path>, sync_on_write: bool) -> ExecutionResult<Self> {
let path = path.as_ref().to_path_buf();
let file = OpenOptions::new()
.create(true)
.append(true)
.read(true)
.open(&path)
.map_err(|err| ExecutionError::Journal(err.to_string()))?;
Ok(Self {
path,
file,
sync_on_write,
})
}
pub fn path(&self) -> &Path {
&self.path
}
fn write_line(&mut self, line: &str) -> ExecutionResult<()> {
self.file
.write_all(line.as_bytes())
.and_then(|()| self.file.write_all(b"\n"))
.map_err(|err| ExecutionError::Journal(err.to_string()))?;
if self.sync_on_write {
self.file
.sync_data()
.map_err(|err| ExecutionError::Journal(err.to_string()))?;
}
Ok(())
}
}
impl ExecutionJournal for FileExecutionJournal {
fn record_command(
&mut self,
kind: JournalCommandKind,
id: ClientOrderId,
ts_ns: u64,
) -> ExecutionResult<()> {
self.write_line(&format!("C|{}|{}|{}", command_kind_u8(kind), id, ts_ns))
}
fn record_event(&mut self, event: &ExecutionEvent) -> ExecutionResult<()> {
self.write_line(&event_to_journal_line(event))
}
fn replay(&self, out: &mut Vec<JournalRecord>) -> ExecutionResult<usize> {
let file =
File::open(&self.path).map_err(|err| ExecutionError::Journal(err.to_string()))?;
let reader = BufReader::new(file);
let start = out.len();
for line in reader.lines() {
let line = line.map_err(|err| ExecutionError::Journal(err.to_string()))?;
if line.is_empty() {
continue;
}
if let Some(record) = parse_journal_line(&line)? {
out.push(record);
}
}
Ok(out.len().saturating_sub(start))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReconciliationAction {
Matched,
VenueOnly,
LocalOnly,
RestateFromVenue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReconciliationItem {
pub client_order_id: ClientOrderId,
pub action: ReconciliationAction,
pub local: Option<OrderState>,
pub venue: Option<OrderState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReconciliationReport {
pub items: Vec<ReconciliationItem>,
}
impl ReconciliationReport {
pub fn is_clean(&self) -> bool {
self.items
.iter()
.all(|item| item.action == ReconciliationAction::Matched)
}
}
pub fn reconcile_open_orders(local: &[OrderState], venue: &[OrderState]) -> ReconciliationReport {
let mut report = ReconciliationReport::default();
let mut venue_by_id: HashMap<ClientOrderId, OrderState> = HashMap::with_capacity(venue.len());
for state in venue {
venue_by_id.insert(state.client_order_id, *state);
}
for local_state in local {
match venue_by_id.remove(&local_state.client_order_id) {
Some(venue_state) if venue_state == *local_state => {
report.items.push(ReconciliationItem {
client_order_id: local_state.client_order_id,
action: ReconciliationAction::Matched,
local: Some(*local_state),
venue: Some(venue_state),
})
}
Some(venue_state) => report.items.push(ReconciliationItem {
client_order_id: local_state.client_order_id,
action: ReconciliationAction::RestateFromVenue,
local: Some(*local_state),
venue: Some(venue_state),
}),
None => report.items.push(ReconciliationItem {
client_order_id: local_state.client_order_id,
action: ReconciliationAction::LocalOnly,
local: Some(*local_state),
venue: None,
}),
}
}
for venue_state in venue_by_id.into_values() {
report.items.push(ReconciliationItem {
client_order_id: venue_state.client_order_id,
action: ReconciliationAction::VenueOnly,
local: None,
venue: Some(venue_state),
});
}
report
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DisconnectPolicy {
Hold = 0,
RejectNew = 1,
CancelOpenOrders = 2,
Freeze = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RouteSafetyPolicy {
pub route: RouteKey,
pub disconnect_policy: DisconnectPolicy,
pub kill_switch: bool,
pub allow_cancels_when_killed: bool,
}
impl RouteSafetyPolicy {
pub const fn reject_new(self, disconnected: bool) -> bool {
self.kill_switch
|| matches!(self.disconnect_policy, DisconnectPolicy::Freeze)
|| (disconnected
&& matches!(
self.disconnect_policy,
DisconnectPolicy::RejectNew | DisconnectPolicy::CancelOpenOrders
))
}
pub const fn allow_cancel(self) -> bool {
!self.kill_switch
|| self.allow_cancels_when_killed
|| matches!(self.disconnect_policy, DisconnectPolicy::RejectNew)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AdvancedRiskLimits {
pub basic: RiskLimits,
pub max_message_rate_per_sec: u32,
pub max_position_abs: i64,
pub max_gross_notional: i128,
pub reduce_only: bool,
}
#[derive(Debug, Default)]
struct MessageRateWindow {
timestamps_ns: VecDeque<u64>,
}
impl MessageRateWindow {
fn allow(&mut self, ts_recv_ns: u64, max_rate: u32) -> bool {
if max_rate == 0 {
return true;
}
let cutoff = ts_recv_ns.saturating_sub(1_000_000_000);
while self
.timestamps_ns
.front()
.is_some_and(|timestamp| *timestamp <= cutoff)
{
self.timestamps_ns.pop_front();
}
if self.timestamps_ns.len() >= max_rate as usize {
return false;
}
self.timestamps_ns.push_back(ts_recv_ns);
true
}
}
#[derive(Debug)]
pub struct AdvancedRiskGate {
limits: AdvancedRiskLimits,
window: Mutex<MessageRateWindow>,
}
impl AdvancedRiskGate {
pub fn new(limits: AdvancedRiskLimits) -> Self {
Self {
limits,
window: Mutex::new(MessageRateWindow {
timestamps_ns: VecDeque::new(),
}),
}
}
fn check_common(&self, ctx: &RiskContext, ts_recv_ns: u64) -> RiskDecision {
if self.limits.basic.kill_switch {
return reject(RiskRejectReason::KillSwitch, "kill switch active");
}
if ctx.duplicate_client_order_id {
return reject(
RiskRejectReason::DuplicateClientOrderId,
"duplicate client order id",
);
}
if !self
.window
.lock()
.expect("risk window mutex")
.allow(ts_recv_ns, self.limits.max_message_rate_per_sec)
{
return reject(RiskRejectReason::MaxOpenOrders, "message rate exceeded");
}
RiskDecision::allow()
}
}
impl RiskCheck for AdvancedRiskGate {
fn check_new(&self, req: &OrderRequest, ctx: &RiskContext) -> RiskDecision {
let common = self.check_common(ctx, req.ts_recv_ns);
if !common.allowed {
return common;
}
if self.limits.basic.max_order_qty > 0 && req.quantity.0 > self.limits.basic.max_order_qty {
return reject(RiskRejectReason::MaxOrderQty, "max order quantity exceeded");
}
let notional = i128::from(req.quantity.0).saturating_mul(i128::from(req.limit_price.0));
if self.limits.basic.max_order_notional > 0
&& notional > self.limits.basic.max_order_notional
{
return reject(
RiskRejectReason::MaxOrderNotional,
"max order notional exceeded",
);
}
if self.limits.max_gross_notional > 0
&& ctx.open_notional.saturating_add(notional) > self.limits.max_gross_notional
{
return reject(
RiskRejectReason::MaxOpenNotional,
"max gross notional exceeded",
);
}
RiskDecision::allow()
}
fn check_amend(&self, req: &AmendRequest, ctx: &RiskContext) -> RiskDecision {
let common = self.check_common(ctx, req.ts_recv_ns);
if !common.allowed {
return common;
}
if self.limits.basic.max_order_qty > 0 && req.quantity.0 > self.limits.basic.max_order_qty {
return reject(RiskRejectReason::MaxOrderQty, "max order quantity exceeded");
}
RiskDecision::allow()
}
fn check_cancel(&self, req: &CancelRequest, ctx: &RiskContext) -> RiskDecision {
self.check_common(ctx, req.ts_recv_ns)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Position {
pub net_qty: i64,
pub buy_qty: i64,
pub sell_qty: i64,
pub gross_notional: i128,
pub average_price: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PositionKey {
pub account_id: AccountId,
pub strategy_id: StrategyId,
pub symbol: ExecutionSymbol,
}
#[derive(Debug, Default, Clone)]
pub struct PositionLedger {
positions: HashMap<PositionKey, Position>,
}
impl PositionLedger {
pub fn new() -> Self {
Self::default()
}
pub fn apply_fill(&mut self, event: &ExecutionEvent, strategy_id: StrategyId, side: OrderSide) {
if event.exec_type != ExecutionType::Trade || event.last_qty.0 <= 0 {
return;
}
let key = PositionKey {
account_id: event.account_id,
strategy_id,
symbol: event.symbol,
};
let position = self.positions.entry(key).or_default();
let signed = match side {
OrderSide::Buy => event.last_qty.0,
OrderSide::Sell => -event.last_qty.0,
};
position.net_qty = position.net_qty.saturating_add(signed);
if side == OrderSide::Buy {
position.buy_qty = position.buy_qty.saturating_add(event.last_qty.0);
} else {
position.sell_qty = position.sell_qty.saturating_add(event.last_qty.0);
}
let fill_notional =
i128::from(event.last_qty.0).saturating_mul(i128::from(event.last_price.0));
position.gross_notional = position.gross_notional.saturating_add(fill_notional);
if position.net_qty != 0 {
position.average_price =
(position.gross_notional / i128::from(position.net_qty.abs())) as i64;
} else {
position.average_price = 0;
}
}
pub fn position(&self, key: &PositionKey) -> Option<Position> {
self.positions.get(key).copied()
}
pub fn positions(&self) -> &HashMap<PositionKey, Position> {
&self.positions
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VenueOrderCapabilities {
pub market: bool,
pub limit: bool,
pub stop: bool,
pub stop_limit: bool,
pub tif_day: bool,
pub tif_gtc: bool,
pub tif_ioc: bool,
pub tif_fok: bool,
pub tif_gtd: bool,
}
impl From<ExecutionCapabilities> for VenueOrderCapabilities {
fn from(value: ExecutionCapabilities) -> Self {
Self {
market: value.market,
limit: value.limit,
stop: value.stop,
stop_limit: value.stop_limit,
tif_day: value.tif_day,
tif_gtc: value.tif_gtc,
tif_ioc: value.tif_ioc,
tif_fok: value.tif_fok,
tif_gtd: value.tif_gtd,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NormalizedOrderType {
pub order_type: OrderType,
pub time_in_force: TimeInForce,
}
pub fn normalize_order_type(
order_type: OrderType,
time_in_force: TimeInForce,
capabilities: VenueOrderCapabilities,
) -> Result<NormalizedOrderType, RiskRejectReason> {
let order_supported = match order_type {
OrderType::Market => capabilities.market,
OrderType::Limit => capabilities.limit,
OrderType::Stop => capabilities.stop,
OrderType::StopLimit => capabilities.stop_limit,
};
if !order_supported {
return Err(RiskRejectReason::UnsupportedOrderType);
}
let tif_supported = match time_in_force {
TimeInForce::Day => capabilities.tif_day,
TimeInForce::Gtc => capabilities.tif_gtc,
TimeInForce::Ioc => capabilities.tif_ioc,
TimeInForce::Fok => capabilities.tif_fok,
TimeInForce::Gtd => capabilities.tif_gtd,
};
if !tif_supported {
return Err(RiskRejectReason::UnsupportedTimeInForce);
}
Ok(NormalizedOrderType {
order_type,
time_in_force,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ExecutionTelemetry {
pub command_queue_depth: u32,
pub report_queue_depth: u32,
pub latency_samples: u64,
pub min_latency_ns: u64,
pub max_latency_ns: u64,
pub total_latency_ns: u128,
}
impl ExecutionTelemetry {
pub fn record_latency(&mut self, latency_ns: u64) {
self.latency_samples = self.latency_samples.saturating_add(1);
self.min_latency_ns = if self.min_latency_ns == 0 {
latency_ns
} else {
self.min_latency_ns.min(latency_ns)
};
self.max_latency_ns = self.max_latency_ns.max(latency_ns);
self.total_latency_ns = self.total_latency_ns.saturating_add(u128::from(latency_ns));
}
pub fn average_latency_ns(&self) -> u64 {
if self.latency_samples == 0 {
0
} else {
(self.total_latency_ns / u128::from(self.latency_samples)) as u64
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShardKey {
pub route_id: RouteId,
pub account_id: AccountId,
pub symbol: ExecutionSymbol,
}
impl From<RouteKey> for ShardKey {
fn from(value: RouteKey) -> Self {
Self {
route_id: value.route_id,
account_id: value.account_id,
symbol: value.symbol,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShardRouter {
pub shard_count: usize,
}
impl ShardRouter {
pub const fn new(shard_count: usize) -> Self {
Self { shard_count }
}
pub fn shard_for(&self, key: ShardKey) -> usize {
if self.shard_count == 0 {
return 0;
}
let mut hasher = StableHasher::default();
key.hash(&mut hasher);
(hasher.finish() as usize) % self.shard_count
}
}
#[derive(Debug, Default)]
struct StableHasher(u64);
impl Hasher for StableHasher {
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 ^= u64::from(*byte);
self.0 = self.0.wrapping_mul(0x100000001b3);
}
}
fn finish(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderThrottle {
capacity: u32,
refill_per_sec: u32,
tokens: u32,
last_refill_ns: u64,
}
impl OrderThrottle {
pub const fn new(capacity: u32, refill_per_sec: u32) -> Self {
Self {
capacity,
refill_per_sec,
tokens: capacity,
last_refill_ns: 0,
}
}
pub fn allow(&mut self, now_ns: u64) -> bool {
self.refill(now_ns);
if self.tokens == 0 {
return false;
}
self.tokens -= 1;
true
}
pub const fn tokens(&self) -> u32 {
self.tokens
}
fn refill(&mut self, now_ns: u64) {
if self.last_refill_ns == 0 {
self.last_refill_ns = now_ns;
return;
}
let elapsed_ns = now_ns.saturating_sub(self.last_refill_ns);
let add = elapsed_ns.saturating_mul(u64::from(self.refill_per_sec)) / 1_000_000_000;
if add > 0 {
self.tokens = self.capacity.min(self.tokens.saturating_add(add as u32));
self.last_refill_ns = now_ns;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReplayDecision {
pub ts_recv_ns: u64,
pub command: ExecutionCommand,
}
#[derive(Debug, Clone)]
pub struct ReplayResult {
pub reports: Vec<ExecutionCommandReport>,
pub metrics: ExecutionMetrics,
}
pub fn replay_simulated_oms(
routes: Vec<RouteConfig>,
decisions: &[ReplayDecision],
) -> ExecutionResult<ReplayResult> {
let mut engine = ExecutionEngine::new(
SimExecutionAdapter::default(),
AllowAllRiskGate,
InMemoryJournal::default(),
routes,
);
engine.start()?;
let mut reports = Vec::with_capacity(decisions.len());
let mut events = ExecutionEventBuffer::with_capacity(64);
for (idx, decision) in decisions.iter().enumerate() {
events.clear();
let kind = decision.command.kind();
let result = match decision.command {
ExecutionCommand::Submit(req) => engine.submit(req, &mut events).map(|()| events.len()),
ExecutionCommand::Cancel(req) => engine.cancel(req, &mut events).map(|()| events.len()),
ExecutionCommand::Amend(req) => engine.amend(req, &mut events).map(|()| events.len()),
ExecutionCommand::Poll => engine.poll(&mut events),
ExecutionCommand::RecoverOpenOrders => engine.recover_open_orders(&mut events),
ExecutionCommand::Stop => Ok(0),
};
reports.push(ExecutionCommandReport {
sequence: (idx + 1) as u64,
kind,
result,
events: events.clone(),
});
}
let metrics = engine.metrics();
Ok(ReplayResult { reports, metrics })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderAdapterContext {
pub name: String,
pub routes: Vec<RouteConfig>,
pub lifecycle: ExecutionLifecycleSnapshot,
}
pub trait ExecutionAdapterFactory {
type Adapter: ExecutionAdapter;
fn build(&self, context: &ProviderAdapterContext) -> ExecutionResult<Self::Adapter>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ProviderAdapterSdk;
impl ProviderAdapterSdk {
pub const fn simulated_capabilities() -> ExecutionCapabilities {
ExecutionCapabilities::simulated()
}
pub fn validate_routes(routes: &[RouteConfig]) -> ExecutionResult<()> {
if routes.is_empty() {
return Err(ExecutionError::RouteNotFound);
}
Ok(())
}
}
fn command_kind_u8(kind: JournalCommandKind) -> u8 {
match kind {
JournalCommandKind::Submit => 1,
JournalCommandKind::Cancel => 2,
JournalCommandKind::Amend => 3,
}
}
fn command_kind_from_u8(value: u8) -> Option<JournalCommandKind> {
match value {
1 => Some(JournalCommandKind::Submit),
2 => Some(JournalCommandKind::Cancel),
3 => Some(JournalCommandKind::Amend),
_ => None,
}
}
fn event_to_journal_line(event: &ExecutionEvent) -> String {
format!(
"E|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
event.exec_type as u8,
event.order_status as u8,
event.client_order_id,
event.orig_client_order_id,
event.venue_order_id,
event.execution_id,
event.account_id,
event.route_id,
event.symbol.venue,
event.symbol.instrument,
event.last_qty.0,
event.last_price.0,
event.cumulative_qty.0,
event.leaves_qty.0,
event.average_price.0,
event.ts_exchange_ns,
event.ts_recv_ns,
event.reason as u8,
sanitize_field(event.text.as_str())
)
}
fn parse_journal_line(line: &str) -> ExecutionResult<Option<JournalRecord>> {
let parts: Vec<&str> = line.split('|').collect();
match parts.first().copied() {
Some("C") if parts.len() == 4 => {
let kind = parts[1]
.parse::<u8>()
.ok()
.and_then(command_kind_from_u8)
.ok_or_else(|| ExecutionError::Journal("invalid command kind".to_string()))?;
let client_order_id = ClientOrderId::new(parts[2])
.map_err(|err| ExecutionError::Journal(err.to_string()))?;
let ts_ns = parts[3]
.parse::<u64>()
.map_err(|err| ExecutionError::Journal(err.to_string()))?;
Ok(Some(JournalRecord::Command {
kind,
client_order_id,
ts_ns,
}))
}
Some("E") if parts.len() == 20 => Ok(Some(JournalRecord::Event(Box::new(
parse_event_parts(&parts)?,
)))),
Some(_) => Err(ExecutionError::Journal("invalid journal line".to_string())),
None => Ok(None),
}
}
fn parse_event_parts(parts: &[&str]) -> ExecutionResult<ExecutionEvent> {
Ok(ExecutionEvent {
exec_type: execution_type_from_u8(parse_u8(parts[1])?)?,
order_status: order_status_from_u8(parse_u8(parts[2])?)?,
client_order_id: fixed(parts[3])?,
orig_client_order_id: fixed(parts[4])?,
venue_order_id: fixed(parts[5])?,
execution_id: fixed(parts[6])?,
account_id: fixed(parts[7])?,
route_id: fixed(parts[8])?,
symbol: ExecutionSymbol {
venue: fixed(parts[9])?,
instrument: fixed(parts[10])?,
},
last_qty: OrderQty(parse_i64(parts[11])?),
last_price: OrderPrice(parse_i64(parts[12])?),
cumulative_qty: OrderQty(parse_i64(parts[13])?),
leaves_qty: OrderQty(parse_i64(parts[14])?),
average_price: OrderPrice(parse_i64(parts[15])?),
ts_exchange_ns: parse_u64(parts[16])?,
ts_recv_ns: parse_u64(parts[17])?,
reason: risk_reason_from_u8(parse_u8(parts[18])?)?,
text: fixed(parts[19])?,
})
}
fn fixed<const N: usize>(value: &str) -> ExecutionResult<FixedAscii<N>> {
FixedAscii::new(value).map_err(|err| ExecutionError::Journal(err.to_string()))
}
fn parse_u8(value: &str) -> ExecutionResult<u8> {
value
.parse::<u8>()
.map_err(|err| ExecutionError::Journal(err.to_string()))
}
fn parse_i64(value: &str) -> ExecutionResult<i64> {
value
.parse::<i64>()
.map_err(|err| ExecutionError::Journal(err.to_string()))
}
fn parse_u64(value: &str) -> ExecutionResult<u64> {
value
.parse::<u64>()
.map_err(|err| ExecutionError::Journal(err.to_string()))
}
fn execution_type_from_u8(value: u8) -> ExecutionResult<ExecutionType> {
match value {
1 => Ok(ExecutionType::Ack),
2 => Ok(ExecutionType::Reject),
3 => Ok(ExecutionType::Trade),
4 => Ok(ExecutionType::CancelPending),
5 => Ok(ExecutionType::CancelAck),
6 => Ok(ExecutionType::CancelReject),
7 => Ok(ExecutionType::ReplacePending),
8 => Ok(ExecutionType::ReplaceAck),
9 => Ok(ExecutionType::ReplaceReject),
10 => Ok(ExecutionType::Expire),
11 => Ok(ExecutionType::Status),
12 => Ok(ExecutionType::Restated),
13 => Ok(ExecutionType::AdapterDegraded),
_ => Err(ExecutionError::Journal(
"invalid execution type".to_string(),
)),
}
}
fn order_status_from_u8(value: u8) -> ExecutionResult<OrderStatus> {
match value {
1 => Ok(OrderStatus::PendingNew),
2 => Ok(OrderStatus::New),
3 => Ok(OrderStatus::PartiallyFilled),
4 => Ok(OrderStatus::Filled),
5 => Ok(OrderStatus::PendingCancel),
6 => Ok(OrderStatus::Cancelled),
7 => Ok(OrderStatus::PendingReplace),
8 => Ok(OrderStatus::Replaced),
9 => Ok(OrderStatus::Rejected),
10 => Ok(OrderStatus::Expired),
11 => Ok(OrderStatus::Suspended),
12 => Ok(OrderStatus::Unknown),
_ => Err(ExecutionError::Journal("invalid order status".to_string())),
}
}
fn risk_reason_from_u8(value: u8) -> ExecutionResult<RiskRejectReason> {
match value {
0 => Ok(RiskRejectReason::None),
1 => Ok(RiskRejectReason::KillSwitch),
2 => Ok(RiskRejectReason::AccountDisabled),
3 => Ok(RiskRejectReason::RouteDisabled),
4 => Ok(RiskRejectReason::SymbolDisabled),
5 => Ok(RiskRejectReason::MaxOrderQty),
6 => Ok(RiskRejectReason::MaxOrderNotional),
7 => Ok(RiskRejectReason::MaxOpenOrders),
8 => Ok(RiskRejectReason::MaxOpenNotional),
9 => Ok(RiskRejectReason::PriceBand),
10 => Ok(RiskRejectReason::DuplicateClientOrderId),
11 => Ok(RiskRejectReason::UnsupportedOrderType),
12 => Ok(RiskRejectReason::UnsupportedTimeInForce),
_ => Err(ExecutionError::Journal("invalid risk reason".to_string())),
}
}
fn sanitize_field(value: &str) -> String {
value.replace('|', " ")
}
fn reject(reason: RiskRejectReason, text: &str) -> RiskDecision {
let text = ExecutionText::new(text).unwrap_or_else(|_| ExecutionText::empty());
RiskDecision::reject(reason, text)
}
#[cfg(test)]
mod tests {
use super::*;
fn id<const N: usize>(value: &str) -> FixedAscii<N> {
FixedAscii::new(value).unwrap()
}
fn symbol(instrument: &str) -> ExecutionSymbol {
ExecutionSymbol {
venue: id::<16>("SIM"),
instrument: id::<32>(instrument),
}
}
fn route() -> RouteConfig {
RouteConfig {
route_id: id("SIM"),
account_id: id("ACC"),
symbol: symbol("ES"),
enabled: true,
risk_limits: RiskLimits {
kill_switch: false,
max_order_qty: 100,
max_order_notional: 1_000_000,
max_open_orders: 10,
max_open_notional: 10_000_000,
price_band_ticks: 0,
},
}
}
fn order(client_order_id: &str) -> OrderRequest {
OrderRequest {
client_order_id: id(client_order_id),
account_id: id("ACC"),
route_id: id("SIM"),
strategy_id: id("STRAT"),
symbol: symbol("ES"),
side: OrderSide::Buy,
order_type: OrderType::Limit,
time_in_force: TimeInForce::Day,
quantity: OrderQty(10),
limit_price: OrderPrice(5000),
stop_price: OrderPrice(0),
ts_exchange_ns: 1,
ts_recv_ns: 2,
}
}
#[test]
fn fanout_drops_when_subscriber_queue_is_full() {
let fanout = ExecutionEventFanout::new(1);
let sub = fanout.subscribe();
let req = order("C1");
fanout.publish(ExecutionEvent::accepted(&req, id("V1")));
fanout.publish(ExecutionEvent::accepted(&req, id("V2")));
assert!(sub.try_recv().is_some());
assert_eq!(fanout.dropped_events(), 1);
}
#[test]
fn file_journal_replays_commands_and_events() {
let path =
std::env::temp_dir().join(format!("orderflow-journal-{}.log", std::process::id()));
let _ = std::fs::remove_file(&path);
let mut journal = FileExecutionJournal::open(&path, false).unwrap();
let req = order("C1");
journal
.record_command(
JournalCommandKind::Submit,
req.client_order_id,
req.ts_recv_ns,
)
.unwrap();
journal
.record_event(&ExecutionEvent::accepted(&req, id("V1")))
.unwrap();
drop(journal);
let journal = FileExecutionJournal::open(&path, false).unwrap();
let mut records = Vec::new();
assert_eq!(journal.replay(&mut records).unwrap(), 2);
assert!(matches!(records[0], JournalRecord::Command { .. }));
assert!(matches!(records[1], JournalRecord::Event(_)));
let _ = std::fs::remove_file(&path);
}
#[test]
fn reconciliation_detects_venue_only_state() {
let req = order("C1");
let state = OrderState::pending_new(&req);
let report = reconcile_open_orders(&[], &[state]);
assert_eq!(report.items.len(), 1);
assert_eq!(report.items[0].action, ReconciliationAction::VenueOnly);
}
#[test]
fn throttle_refills_over_time() {
let mut throttle = OrderThrottle::new(1, 1);
assert!(throttle.allow(1));
assert!(!throttle.allow(2));
assert!(throttle.allow(1_000_000_002));
}
#[test]
fn replay_simulation_is_deterministic() {
let decisions = [ReplayDecision {
ts_recv_ns: 2,
command: ExecutionCommand::Submit(order("C1")),
}];
let result = replay_simulated_oms(vec![route()], &decisions).unwrap();
assert_eq!(result.reports.len(), 1);
assert_eq!(result.reports[0].events.len(), 2);
assert_eq!(result.metrics.submitted, 1);
}
#[test]
fn normalize_rejects_unsupported_tif() {
let caps = VenueOrderCapabilities {
market: true,
limit: true,
stop: false,
stop_limit: false,
tif_day: true,
tif_gtc: false,
tif_ioc: false,
tif_fok: false,
tif_gtd: false,
};
assert_eq!(
normalize_order_type(OrderType::Limit, TimeInForce::Gtc, caps).unwrap_err(),
RiskRejectReason::UnsupportedTimeInForce
);
}
}