use std::fmt::{self, Display};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use solana_pubkey::Pubkey;
use crate::types::core::{Decimal, Side};
use crate::types::market::{RiskState, RiskTier};
use crate::types::trader::TraderCapabilitiesView;
use crate::types::trader_key::TraderKey;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
Open,
Filled,
Cancelled,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderHistoryItem {
pub order_sequence_number: String,
pub market_symbol: String,
pub status: OrderStatus,
pub side: Side,
pub is_reduce_only: bool,
#[serde(default)]
pub is_stop_loss: bool,
pub price: String,
pub base_qty: String,
pub remaining_base_qty: String,
pub filled_base_qty: String,
pub placed_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
}
pub type OrderHistoryResponse = crate::types::core::PaginatedResponse<Vec<OrderHistoryItem>>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderHistoryQueryParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub trader_pda_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_symbol: Option<String>,
pub limit: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privy_id: Option<String>,
}
impl OrderHistoryQueryParams {
pub fn new(limit: i64) -> Self {
Self {
trader_pda_index: None,
market_symbol: None,
limit,
cursor: None,
privy_id: None,
}
}
pub fn with_pda_index(mut self, pda_index: u8) -> Self {
self.trader_pda_index = Some(pda_index);
self
}
pub fn with_market_symbol(mut self, symbol: impl Into<String>) -> Self {
self.market_symbol = Some(symbol.into());
self
}
pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
pub fn with_privy_id(mut self, privy_id: impl Into<String>) -> Self {
self.privy_id = Some(privy_id.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollateralHistoryRequest {
pub limit: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl CollateralHistoryRequest {
pub fn new(limit: i64) -> Self {
Self {
limit,
next_cursor: None,
prev_cursor: None,
cursor: None,
}
}
pub fn with_next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.next_cursor = Some(cursor.into());
self
}
pub fn with_prev_cursor(mut self, cursor: impl Into<String>) -> Self {
self.prev_cursor = Some(cursor.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollateralHistoryQueryParams {
#[serde(default, alias = "pdaIndex", alias = "pda_index")]
pub pda_index: u8,
pub request: CollateralHistoryRequest,
}
impl CollateralHistoryQueryParams {
pub fn new(limit: i64) -> Self {
Self {
pda_index: 0,
request: CollateralHistoryRequest::new(limit),
}
}
pub fn with_pda_index(mut self, pda_index: u8) -> Self {
self.pda_index = pda_index;
self
}
pub fn with_next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request.next_cursor = Some(cursor.into());
self
}
pub fn with_prev_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request.prev_cursor = Some(cursor.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollateralHistoryResponse {
pub data: Vec<CollateralEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollateralEvent {
pub slot: i64,
pub slot_index: i32,
pub event_index: i32,
pub trader_pda_index: i32,
pub trader_subaccount_index: i32,
pub event_type: String,
pub amount: i64,
pub collateral_after: i64,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingHistoryQueryParams {
#[serde(default)]
pub pda_index: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl FundingHistoryQueryParams {
pub fn new() -> Self {
Self::default()
}
pub fn with_pda_index(mut self, pda_index: u8) -> Self {
self.pda_index = pda_index;
self
}
pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = Some(symbol.into());
self
}
pub fn with_start_time(mut self, start_time: i64) -> Self {
self.start_time = Some(start_time);
self
}
pub fn with_end_time(mut self, end_time: i64) -> Self {
self.end_time = Some(end_time);
self
}
pub fn with_limit(mut self, limit: i64) -> Self {
self.limit = Some(limit);
self
}
pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingHistoryResponse {
pub events: Vec<FundingHistoryEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingHistoryEvent {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub symbol: String,
pub funding_payment: String,
pub funding_rate_percentage: String,
pub position_size: String,
pub position_side: String,
}
#[cfg(test)]
mod tests {
use super::FundingHistoryEvent;
#[test]
fn funding_history_timestamp_accepts_rfc3339() {
let raw = r#"{
"timestamp":"2026-02-11T16:00:00Z",
"symbol":"SOL",
"fundingPayment":"-0.123",
"fundingRatePercentage":"0.001",
"positionSize":"10",
"positionSide":"Long"
}"#;
let event: FundingHistoryEvent =
serde_json::from_str(raw).expect("RFC3339 timestamp should deserialize");
assert_eq!(event.timestamp.to_rfc3339(), "2026-02-11T16:00:00+00:00");
}
#[test]
fn funding_history_timestamp_rejects_integer() {
let raw = r#"{
"timestamp":1770825600,
"symbol":"SOL",
"fundingPayment":"-0.123",
"fundingRatePercentage":"0.001",
"positionSize":"10",
"positionSide":"Long"
}"#;
let result = serde_json::from_str::<FundingHistoryEvent>(raw);
assert!(
result.is_err(),
"integer timestamp should not deserialize for FundingHistoryEvent"
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PnlResolution {
#[serde(rename = "1m")]
Minute1,
#[serde(rename = "5m")]
Minute5,
#[serde(rename = "15m")]
Minute15,
#[serde(rename = "1h")]
Hour1,
#[serde(rename = "4h")]
Hour4,
#[serde(rename = "1d")]
Day1,
#[serde(rename = "1w")]
Week1,
#[serde(rename = "1M")]
Month1,
}
impl Display for PnlResolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PnlResolution::Minute1 => write!(f, "1m"),
PnlResolution::Minute5 => write!(f, "5m"),
PnlResolution::Minute15 => write!(f, "15m"),
PnlResolution::Hour1 => write!(f, "1h"),
PnlResolution::Hour4 => write!(f, "4h"),
PnlResolution::Day1 => write!(f, "1d"),
PnlResolution::Week1 => write!(f, "1w"),
PnlResolution::Month1 => write!(f, "1M"),
}
}
}
impl FromStr for PnlResolution {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1m" => Ok(PnlResolution::Minute1),
"5m" => Ok(PnlResolution::Minute5),
"15m" => Ok(PnlResolution::Minute15),
"1h" => Ok(PnlResolution::Hour1),
"4h" => Ok(PnlResolution::Hour4),
"1d" => Ok(PnlResolution::Day1),
"1w" => Ok(PnlResolution::Week1),
"1M" => Ok(PnlResolution::Month1),
_ => Err(format!("Unknown PnL resolution: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PnlQueryParams {
pub resolution: PnlResolution,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
}
impl PnlQueryParams {
pub fn new(resolution: PnlResolution) -> Self {
Self {
resolution,
start_time: None,
end_time: None,
limit: None,
}
}
pub fn with_start_time(mut self, start_time: i64) -> Self {
self.start_time = Some(start_time);
self
}
pub fn with_end_time(mut self, end_time: i64) -> Self {
self.end_time = Some(end_time);
self
}
pub fn with_limit(mut self, limit: i64) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PnlPoint {
pub timestamp: i64,
pub start_time: i64,
pub end_time: i64,
pub cumulative_pnl: f64,
pub unrealized_pnl: f64,
pub cumulative_funding_payment: f64,
pub cumulative_taker_fee: f64,
}
pub type PnlResponse = Vec<PnlPoint>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum TraderActivityState {
Uninitialized,
Cold,
Active,
ReduceOnly,
Frozen,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraderPositionView {
pub symbol: String,
pub position_size: Decimal,
pub virtual_quote_position: Decimal,
pub entry_price: Decimal,
pub unrealized_pnl: Decimal,
pub discounted_unrealized_pnl: Decimal,
pub position_initial_margin: Decimal,
pub initial_margin: Decimal,
pub maintenance_margin: Decimal,
pub backstop_margin: Decimal,
pub limit_order_margin: Decimal,
pub position_value: Decimal,
pub unsettled_funding: Decimal,
pub accumulated_funding: Decimal,
pub liquidation_price: Decimal,
#[serde(default)]
pub take_profit_price: Option<Decimal>,
#[serde(default)]
pub stop_loss_price: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LimitOrder {
pub price: Decimal,
pub side: Side,
pub order_sequence_number: String,
pub initial_trade_size: Decimal,
pub trade_size_remaining: Decimal,
pub margin_requirement: Decimal,
pub margin_factor: Decimal,
pub is_reduce_only: bool,
#[serde(default)]
pub is_stop_loss: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraderView {
pub flags: u16,
pub state: TraderActivityState,
pub capabilities: TraderCapabilitiesView,
pub trader_key: String,
pub trader_pda_index: u8,
pub trader_subaccount_index: u8,
pub authority: String,
pub collateral_balance: Decimal,
pub effective_collateral: Decimal,
pub effective_collateral_for_withdrawals: Decimal,
pub unrealized_pnl: Decimal,
pub discounted_unrealized_pnl: Decimal,
pub unsettled_funding_owed: Decimal,
pub accumulated_funding: Decimal,
pub portfolio_value: Decimal,
pub maintenance_margin: Decimal,
pub cancel_margin: Decimal,
pub initial_margin: Decimal,
pub initial_margin_for_withdrawals: Decimal,
pub risk_state: RiskState,
pub risk_tier: RiskTier,
pub positions: Vec<TraderPositionView>,
pub limit_orders: std::collections::HashMap<String, Vec<LimitOrder>>,
pub maker_fee_override_multiplier: f64,
pub taker_fee_override_multiplier: f64,
pub max_positions: u64,
pub last_deposit_slot: u64,
pub is_in_active_traders: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraderStateResponse {
pub slot: u64,
pub slot_index: u32,
pub authority: String,
pub pda_index: u8,
pub traders: Vec<TraderView>,
}
impl TraderStateResponse {
pub fn isolated_subaccount_for_asset(&self, symbol: &str) -> Option<&TraderView> {
if let Some(t) = self.traders.iter().find(|t| {
t.trader_subaccount_index > 0 && t.positions.iter().any(|p| p.symbol == symbol)
}) {
return Some(t);
}
self.traders
.iter()
.find(|t| t.trader_subaccount_index > 0 && t.positions.is_empty())
}
pub fn get_next_isolated_subaccount_key(&self) -> Option<TraderKey> {
let authority: Pubkey = self.authority.parse().ok()?;
let registered: std::collections::HashSet<u8> = self
.traders
.iter()
.map(|t| t.trader_subaccount_index)
.collect();
let idx = (1..=255u8).find(|idx| !registered.contains(idx))?;
Some(TraderKey::new_with_idx(authority, self.pda_index, idx))
}
}