use betex::{
book::protocol::{
command::{
BinarySide, BinaryTimeInForce, MarketState, Persistence, RunnerChange, Side,
TimeInForce,
},
response::{Response, TradeSummary},
},
snapshot::MarketSnapshot,
types::{
AccountId, BookType, CorrelationId, MarketId, MarketKind, MarketModel, MarketPhase,
OddsX10000, OrderId, RunnerId,
},
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum MarketStatus {
Active,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MarketInfo {
pub market_id: MarketId,
pub engine_id: Uuid,
pub name: String,
pub market_model: MarketModel,
pub market_kind: MarketKind,
pub market_phase: MarketPhase,
pub runner_ids: Vec<RunnerId>,
pub runner_labels: Vec<String>,
pub status: MarketStatus,
pub created_at: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum ClientMsg {
SetMarketState {
#[serde(default)]
market_id: Option<MarketId>,
state: MarketState,
},
AwaitLiveMarket {
#[serde(default)]
market_id: Option<MarketId>,
},
GoLiveMarket {
#[serde(default)]
market_id: Option<MarketId>,
},
CloseMarket {
#[serde(default)]
market_id: Option<MarketId>,
#[serde(default)]
reason: String,
},
BatchCancelOrders {
#[serde(default)]
correlation_id: Option<CorrelationId>,
#[serde(default)]
market_id: Option<MarketId>,
#[serde(default)]
from_created_at_inclusive_ms: Option<i64>,
#[serde(default)]
to_created_at_inclusive_ms: Option<i64>,
#[serde(default)]
account_id: Option<AccountId>,
#[serde(default)]
runner_id: Option<RunnerId>,
reason: String,
#[serde(default)]
metadata: Option<serde_json::Value>,
},
ContinueBatchProcess {
#[serde(default)]
correlation_id: Option<CorrelationId>,
#[serde(default)]
market_id: Option<MarketId>,
},
HaltMarket {
#[serde(default)]
market_id: Option<MarketId>,
reason: String,
},
ResumeMarket {
#[serde(default)]
market_id: Option<MarketId>,
},
Ping,
CreateMarket {
name: String,
#[serde(default)]
market_model: MarketModel,
book_type: Option<BookType>,
market_kind: MarketKind,
#[serde(default)]
market_state: MarketState,
market_phase: MarketPhase,
runner_ids: Vec<RunnerId>,
runner_labels: Vec<String>,
},
AddRunners {
#[serde(default)]
market_id: Option<MarketId>,
runner_ids: Vec<RunnerId>,
runner_labels: Vec<String>,
},
ChangeRunners {
#[serde(default)]
market_id: Option<MarketId>,
add: Vec<RunnerChange>,
remove: Vec<RunnerChange>,
},
ListMarkets,
Subscribe {
market_id: MarketId,
},
PlaceOrder {
#[serde(default)]
correlation_id: Option<CorrelationId>,
order_id: OrderId,
market_id: MarketId,
runner_id: RunnerId,
account_id: AccountId,
side: Side,
odds: OddsX10000,
stake: i64,
persistence: Option<Persistence>,
time_in_force: Option<TimeInForce>,
},
PlaceBinaryOrder {
#[serde(default)]
correlation_id: Option<CorrelationId>,
order_id: OrderId,
market_id: MarketId,
account_id: AccountId,
side: BinarySide,
price_ticks: u16,
qty_shares: u64,
time_in_force: Option<BinaryTimeInForce>,
},
CancelOrder {
#[serde(default)]
correlation_id: Option<CorrelationId>,
market_id: MarketId,
account_id: AccountId,
order_id: OrderId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum ServerMsg {
Snapshot {
snapshot: MarketSnapshot,
},
Status {
global_seq: u64,
market_count: usize,
},
Ack {
ok: bool,
response: Option<Response>,
error: Option<String>,
},
OrderResponse {
correlation_id: Option<CorrelationId>,
ok: bool,
order_id: Option<OrderId>,
trades: Vec<TradeSummary>,
error: Option<String>,
},
MarketCreated {
market: MarketInfo,
},
MarketUpdated {
market: MarketInfo,
},
MarketRemoved {
market_id: MarketId,
},
MarketList {
markets: Vec<MarketInfo>,
},
MarketStatusChanged {
market_id: MarketId,
status: MarketStatus,
},
MarketPhaseChanged {
market_id: MarketId,
phase: MarketPhase,
},
}
pub(crate) fn encode_msg<T: Serialize + ?Sized>(value: &T) -> Result<String, serde_json::Error> {
serde_json::to_string(value)
}
pub(crate) fn decode_msg<T>(data: &[u8]) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
serde_json::from_slice(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_market_json_missing_book_type_decodes_to_none() {
let value = serde_json::json!({
"CREATE_MARKET": {
"name": "x",
"market_model": "EXCHANGE_ODDS",
"market_kind": "IN_PLAY_CAPABLE",
"market_state": "OPEN",
"market_phase": "PRE",
"runner_ids": [1, 2],
"runner_labels": ["A", "B"]
}
});
let decoded = serde_json::from_value::<ClientMsg>(value).expect("decode create market");
match decoded {
ClientMsg::CreateMarket { book_type, .. } => assert_eq!(book_type, None),
other => panic!("expected CreateMarket, got {other:?}"),
}
}
#[test]
fn change_runners_json_roundtrip() {
let msg = ClientMsg::ChangeRunners {
market_id: Some(MarketId(7)),
add: vec![RunnerChange {
runner_id: RunnerId(3),
runner_label: "C".to_string(),
}],
remove: vec![RunnerChange {
runner_id: RunnerId(1),
runner_label: "A".to_string(),
}],
};
let value = serde_json::to_value(&msg).expect("serialize change runners");
let decoded = serde_json::from_value::<ClientMsg>(value).expect("decode change runners");
match decoded {
ClientMsg::ChangeRunners {
market_id,
add,
remove,
} => {
assert_eq!(market_id, Some(MarketId(7)));
assert_eq!(add[0].runner_id, RunnerId(3));
assert_eq!(add[0].runner_label, "C");
assert_eq!(remove[0].runner_id, RunnerId(1));
assert_eq!(remove[0].runner_label, "A");
}
other => panic!("expected ChangeRunners, got {other:?}"),
}
}
}