use serde::{Deserialize, Serialize};
use crate::types::core::Side;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateServerMessage {
pub authority: String,
pub trader_pda_index: u8,
pub slot: u64,
#[serde(flatten)]
pub content: TraderStatePayload,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "messageType", rename_all = "camelCase")]
pub enum TraderStatePayload {
#[serde(rename = "snapshot")]
Snapshot(TraderStateSnapshot),
#[serde(rename = "delta")]
Delta(TraderStateDelta),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSnapshot {
pub version: u32,
pub capabilities: TraderStateCapabilities,
pub maker_fee_override_multiplier: f64,
pub taker_fee_override_multiplier: f64,
pub subaccounts: Vec<TraderStateSubaccountSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateDelta {
pub deltas: Vec<TraderStateSubaccountDelta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSubaccountSnapshot {
pub subaccount_index: u8,
pub sequence: u64,
pub collateral: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<TraderStateCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cooldown_status: Option<CooldownStatus>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub positions: Vec<TraderStatePositionSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub orders: Vec<TraderStateLimitOrderEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub splines: Vec<TraderStateSplineSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSubaccountDelta {
pub subaccount_index: u8,
pub sequence: u64,
pub collateral: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<TraderStateCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cooldown_status: Option<CooldownStatus>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub positions: Vec<TraderStatePositionDelta>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub orders: Vec<TraderStateLimitOrderEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub splines: Vec<TraderStateSplineDelta>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub trade_history: Vec<TradeHistoryDelta>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub order_history: Vec<OrderHistoryDelta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStatePositionSnapshot {
pub symbol: String,
#[serde(flatten)]
pub position: TraderStatePositionRow,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStatePositionRow {
pub position_sequence_number: String,
pub base_position_lots: String,
pub entry_price_ticks: String,
pub entry_price_usd: String,
pub virtual_quote_position_lots: String,
pub unsettled_funding_quote_lots: String,
pub accumulated_funding_quote_lots: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub take_profit_triggers: Vec<TraderStateTakeProfitTrigger>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_loss_triggers: Vec<TraderStateStopLossTrigger>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateTrigger {
pub trigger_price_ticks: String,
pub execution_price_ticks: String,
pub side: Side,
pub kind: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStatePositionDelta {
pub symbol: String,
pub change: TraderStateRowChangeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<TraderStatePositionRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum TraderStateRowChangeKind {
Updated,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateStopLossTrigger {
pub stop_loss_id: String,
pub trigger: TraderStateTrigger,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateTakeProfitTrigger {
pub take_profit_id: String,
pub trigger: TraderStateTrigger,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateLimitOrderEvent {
pub symbol: String,
pub orders: Vec<TraderStateMarketLimitOrderEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateMarketLimitOrderEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub change: Option<TraderStateRowChangeKind>,
pub order_sequence_number: String,
pub side: Side,
pub order_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub conditional_kind: Option<String>,
pub price_ticks: String,
pub price_usd: String,
pub size_remaining_lots: String,
pub initial_size_lots: String,
pub reduce_only: bool,
#[serde(default)]
pub is_stop_loss: bool,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSplineSnapshot {
pub symbol: String,
#[serde(flatten)]
pub spline: TraderStateSplineRow,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSplineRow {
pub mid_price_ticks: String,
pub bid_filled_amount_lots: String,
pub ask_filled_amount_lots: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bid_regions: Vec<TraderStateTickRegion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ask_regions: Vec<TraderStateTickRegion>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateTickRegion {
pub start_price_ticks: String,
pub end_price_ticks: String,
pub density_lots_per_tick: String,
pub total_size_lots: String,
pub filled_size_lots: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateSplineDelta {
pub symbol: String,
pub change: TraderStateRowChangeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub spline: Option<TraderStateSplineRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TradeHistoryDelta {
pub signature: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fill_id: Option<String>,
pub timestamp: u64,
pub slot: i64,
pub slot_index: i32,
pub instruction_index: i32,
pub event_index: i32,
pub market: String,
pub instruction_type: String,
pub trade_type: String,
pub base_qty_before: String,
pub base_qty_after: String,
pub size: String,
pub liquidity: String,
pub price: String,
pub fee: String,
pub realized_pnl: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct OrderHistoryDelta {
pub timestamp: u64,
pub slot: u64,
pub slot_index: u32,
pub instruction_index: u32,
pub event_index: u32,
pub market: String,
pub instruction_type: String,
pub order_type: String,
pub status: String,
pub size: String,
pub price: String,
pub filled_size: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CooldownStatus {
pub last_deposit_slot: u64,
pub cooldown_period_in_slots: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateCapabilities {
pub flags: u16,
pub state: String,
pub capabilities: TraderCapabilitiesView,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TraderCapabilitiesView {
pub place_limit_order: CapabilityAccess,
pub place_market_order: CapabilityAccess,
pub risk_increasing_trade: CapabilityAccess,
pub risk_reducing_trade: CapabilityAccess,
pub deposit_collateral: CapabilityAccess,
pub withdraw_collateral: CapabilityAccess,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityAccess {
#[serde(default)]
pub immediate: bool,
#[serde(default)]
pub via_cold_activation: bool,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_trader_state_snapshot() {
let json = r#"{
"authority": "ABC123",
"traderPdaIndex": 0,
"slot": 12345,
"messageType": "snapshot",
"version": 1,
"capabilities": {
"flags": 63,
"state": "active",
"capabilities": {
"placeLimitOrder": {"immediate": true},
"placeMarketOrder": {"immediate": true},
"riskIncreasingTrade": {"immediate": true},
"riskReducingTrade": {"immediate": true},
"depositCollateral": {"immediate": true},
"withdrawCollateral": {"immediate": true}
}
},
"makerFeeOverrideMultiplier": 1.0,
"takerFeeOverrideMultiplier": 1.0,
"subaccounts": []
}"#;
let msg: TraderStateServerMessage = serde_json::from_str(json).unwrap();
assert_eq!(msg.authority, "ABC123");
assert_eq!(msg.slot, 12345);
assert!(matches!(msg.content, TraderStatePayload::Snapshot(_)));
}
#[test]
fn test_deserialize_trader_state_delta_trade_history_fill_id() {
let json = json!({
"authority": "ABC123",
"traderPdaIndex": 0,
"slot": 12346,
"messageType": "delta",
"deltas": [{
"subaccountIndex": 0,
"sequence": 101,
"collateral": "1000",
"positions": [],
"orders": [],
"splines": [],
"tradeHistory": [{
"signature": "test_signature",
"fillId": "43148c7f-1389-34f5-99f0-c7296f5858c2",
"timestamp": 1000,
"slot": 100,
"slotIndex": 1,
"instructionIndex": 2,
"eventIndex": 3,
"market": "SOL-PERP",
"instructionType": "PlaceMarketOrder",
"tradeType": "market",
"baseQtyBefore": "0",
"baseQtyAfter": "1",
"size": "1",
"liquidity": "taker",
"price": "150",
"fee": "0.1",
"realizedPnl": "1"
}],
"orderHistory": []
}]
});
let msg: TraderStateServerMessage = serde_json::from_value(json).unwrap();
let TraderStatePayload::Delta(delta) = msg.content else {
panic!("expected delta payload");
};
assert_eq!(
delta.deltas[0].trade_history[0].fill_id,
Some("43148c7f-1389-34f5-99f0-c7296f5858c2".to_string())
);
}
}