use serde::{Deserialize, Deserializer};
use crate::client::http::{ApiError, Call, Transport, read};
use crate::markets::books::MarketStatus;
use crate::markets::listed;
use crate::utils::dec::Dec;
use crate::utils::figure::Figure;
use crate::utils::ids::whole;
use crate::utils::{AccountIndex, AssetId, Grid, MarketId, MarketKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Outcome {
#[default]
Undetermined,
No,
Yes,
Other(u8),
}
impl Outcome {
#[must_use]
pub const fn from_code(code: u8) -> Outcome {
match code {
0 => Outcome::Undetermined,
1 => Outcome::No,
2 => Outcome::Yes,
other => Outcome::Other(other),
}
}
#[must_use]
pub const fn code(self) -> u8 {
match self {
Outcome::Undetermined => 0,
Outcome::No => 1,
Outcome::Yes => 2,
Outcome::Other(code) => code,
}
}
}
impl<'de> Deserialize<'de> for Outcome {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Outcome::from_code(u8::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct BinaryStats {
pub symbol: String,
pub market_id: MarketId,
pub mid_price: Dec,
pub best_ask_price: Dec,
pub best_bid_price: Dec,
pub last_trade_price: Dec,
pub daily_base_token_volume: Figure,
pub daily_quote_token_volume: Figure,
pub daily_price_low: Figure,
pub daily_price_high: Figure,
pub daily_price_change: Figure,
pub lifetime_quote_token_volume: Figure,
pub lifetime_base_token_volume: Figure,
pub open_interest: Dec,
pub status: MarketStatus,
pub outcome: Outcome,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct BinaryMarket {
pub symbol: String,
pub market_id: MarketId,
#[serde(default, deserialize_with = "kind")]
pub market_type: Option<MarketKind>,
pub base_asset_id: AssetId,
pub quote_asset_id: AssetId,
pub status: MarketStatus,
pub taker_fee: Dec,
pub maker_fee: Dec,
#[serde(default)]
pub is_taker_fee_enabled: bool,
#[serde(default)]
pub is_maker_fee_enabled: bool,
pub liquidation_fee: Dec,
pub min_base_amount: Dec,
pub min_quote_amount: Dec,
pub order_quote_limit: Dec,
pub supported_size_decimals: u32,
pub supported_price_decimals: u32,
pub supported_quote_decimals: u32,
pub size_decimals: u32,
pub price_decimals: u32,
#[serde(deserialize_with = "whole")]
pub created_at: u64,
pub multiplier: Dec,
#[serde(deserialize_with = "whole")]
pub start_timestamp: u64,
#[serde(deserialize_with = "whole")]
pub end_timestamp: u64,
pub open_interest: Dec,
pub outcome: Outcome,
pub operator_account_index: AccountIndex,
pub settlement_cap: u32,
pub settlement_type: u8,
pub settlement_price: u32,
pub default_price: u32,
pub is_frozen: bool,
#[serde(default)]
pub market_deployer: Option<String>,
#[serde(default)]
pub stats: Option<BinaryStats>,
}
impl BinaryMarket {
#[must_use]
pub const fn grid(&self) -> Grid {
Grid::new(self.size_decimals, self.price_decimals)
}
#[must_use]
pub fn is_open(&self, now_ms: u64) -> bool {
self.status == MarketStatus::Active
&& !self.is_frozen
&& self.start_timestamp <= now_ms
&& (self.end_timestamp == 0 || now_ms <= self.end_timestamp)
}
#[must_use]
pub fn payout(&self) -> Dec {
self.grid().price_of(self.settlement_cap)
}
#[must_use]
pub fn settled_at(&self) -> Option<Dec> {
(self.status == MarketStatus::Settled).then(|| self.grid().price_of(self.settlement_price))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct EventMetadata {
#[serde(rename = "type", default)]
pub kind: Option<String>,
#[serde(default)]
pub symbol: Option<String>,
#[serde(default)]
pub duration: Option<String>,
#[serde(default)]
pub provider: Option<String>,
#[serde(default)]
pub feed_id: Option<String>,
#[serde(default)]
pub market_id: Option<MarketId>,
#[serde(default)]
pub underlying_market_id: Option<MarketId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
OneOff,
Recurrent,
#[serde(other)]
Other,
}
impl EventKind {
#[must_use]
pub const fn wire(self) -> &'static str {
match self {
EventKind::OneOff => "one_off",
EventKind::Recurrent => "recurrent",
EventKind::Other => "",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Event {
#[serde(deserialize_with = "whole")]
pub event_id: u64,
pub title: String,
#[serde(default)]
pub description: String,
pub creator_account_index: AccountIndex,
#[serde(deserialize_with = "whole")]
pub created_at: u64,
pub tag: String,
pub event_kind: EventKind,
#[serde(default, deserialize_with = "whole")]
pub recurrence_period_seconds: u64,
#[serde(deserialize_with = "whole")]
pub start_at: u64,
#[serde(deserialize_with = "whole")]
pub end_at: u64,
#[serde(default)]
pub metadata: EventMetadata,
#[serde(default, deserialize_with = "listed")]
pub order_books: Vec<BinaryMarket>,
pub lifetime_quote_token_volume: Figure,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct EventPage {
#[serde(default, deserialize_with = "listed")]
pub events: Vec<Event>,
#[serde(default)]
pub next_cursor: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum EventSort {
#[default]
Id,
StartAt,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EventQuery {
pub event_ids: Vec<u64>,
pub market_ids: Vec<MarketId>,
pub creator: Option<AccountIndex>,
pub cursor: Option<u64>,
pub active: Option<bool>,
pub limit: Option<u32>,
pub tag: Option<String>,
pub event_kind: Option<EventKind>,
pub start_from: Option<u64>,
pub start_before: Option<u64>,
pub sort: EventSort,
pub ascending: bool,
}
impl EventQuery {
#[must_use]
pub fn tradable(limit: u32) -> EventQuery {
EventQuery {
active: Some(true),
limit: Some(limit),
sort: EventSort::StartAt,
ascending: true,
..EventQuery::default()
}
}
#[must_use]
pub fn series(self, tag: impl Into<String>) -> EventQuery {
EventQuery { tag: Some(tag.into()), ..self }
}
#[must_use]
pub fn of_markets(markets: &[MarketId]) -> EventQuery {
EventQuery { market_ids: markets.to_vec(), ..EventQuery::default() }
}
#[must_use]
pub fn after(self, cursor: u64) -> EventQuery {
EventQuery { cursor: Some(cursor), ..self }
}
}
fn joined<T: ToString>(ids: &[T]) -> Option<String> {
(!ids.is_empty()).then(|| ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(","))
}
#[must_use]
pub fn events_call(query: &EventQuery) -> Call {
Call::get("events")
.maybe("event_ids", joined(&query.event_ids))
.maybe("market_ids", joined(&query.market_ids))
.maybe("creator_account_index", query.creator)
.maybe("cursor", query.cursor)
.maybe("active", query.active)
.maybe("limit", query.limit.map(|limit| limit.clamp(1, 100)))
.maybe("tag", query.tag.as_deref())
.maybe("event_kind", query.event_kind.map(EventKind::wire))
.maybe("start_from", query.start_from)
.maybe("start_before", query.start_before)
.maybe("sort", (query.sort == EventSort::StartAt).then_some("start_at"))
.maybe("order", query.ascending.then_some("asc"))
}
pub async fn events(transport: &impl Transport, query: &EventQuery) -> Result<EventPage, ApiError> {
read(transport, events_call(query)).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum UpDownResolution {
#[default]
Second,
ThirtySeconds,
}
impl UpDownResolution {
#[must_use]
pub const fn wire(self) -> &'static str {
match self {
UpDownResolution::Second => "1s",
UpDownResolution::ThirtySeconds => "30s",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<UpDownResolution> {
[UpDownResolution::Second, UpDownResolution::ThirtySeconds]
.into_iter()
.find(|resolution| resolution.wire() == text)
}
}
impl std::fmt::Display for UpDownResolution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.wire())
}
}
pub const MOST_UP_DOWN_CANDLES: u32 = 3_600;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct UpDownCandle {
pub symbol: String,
pub duration: String,
pub resolution: String,
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub open: Dec,
pub high: Dec,
pub low: Dec,
pub close: Dec,
#[serde(default)]
pub provider: String,
#[serde(default)]
pub feed_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct UpDownPage {
#[serde(default, deserialize_with = "listed")]
pub candles: Vec<UpDownCandle>,
#[serde(default, deserialize_with = "whole")]
pub next_cursor: u64,
}
impl UpDownPage {
#[must_use]
pub const fn next(&self) -> Option<u64> {
match self.next_cursor {
0 => None,
cursor => Some(cursor),
}
}
}
#[must_use]
pub fn up_down_candles_call(
symbol: &str,
duration: &str,
resolution: UpDownResolution,
start_s: u64,
end_s: u64,
limit: Option<u32>,
) -> Call {
Call::get("upDownCandles")
.field("symbol", symbol)
.field("duration", duration)
.field("resolution", resolution.wire())
.field("start_timestamp", start_s)
.field("end_timestamp", end_s)
.maybe("limit", limit.map(|limit| limit.clamp(1, MOST_UP_DOWN_CANDLES)))
}
pub async fn up_down_candles(
transport: &impl Transport,
symbol: &str,
duration: &str,
resolution: UpDownResolution,
start_s: u64,
end_s: u64,
limit: Option<u32>,
) -> Result<UpDownPage, ApiError> {
read(transport, up_down_candles_call(symbol, duration, resolution, start_s, end_s, limit)).await
}
fn kind<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<MarketKind>, D::Error> {
crate::utils::order::known(deserializer)
}
#[cfg(test)]
#[path = "tests/binary.rs"]
mod tests;