use std::collections::BTreeMap;
use crate::markets::outcomes::coin::Coin;
use crate::markets::outcomes::fields::Fields;
use crate::markets::outcomes::meta::{Outcome as RawOutcome, OutcomeMeta, Question as RawQuestion};
use crate::markets::outcomes::template::{Family, Pattern, Patterns, Template};
use crate::markets::outcomes::time::Stamp;
use crate::utils::{Dec, OutcomeSide};
mod interpret;
pub use interpret::{Details, Pair};
use interpret::{bucket_titles, from_pattern, interpret, label};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Status {
Open,
Expired,
}
impl Status {
#[must_use]
pub fn at(expiry: Option<Stamp>, now_ms: u64) -> Status {
match expiry {
Some(deadline) if deadline.passed(now_ms) => Status::Expired,
_ => Status::Open,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sides {
pub yes: Coin,
pub no: Coin,
pub yes_label: String,
pub no_label: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Market {
pub market: Coin,
pub outcome: u32,
pub kind: Family,
pub title: String,
pub template: String,
pub template_version: Option<u8>,
pub description: String,
pub status: Status,
pub expiry: Option<Stamp>,
pub venue: Option<String>,
pub quote: Option<String>,
pub fee_scale: Option<Dec>,
pub question: Option<u32>,
pub question_title: Option<String>,
pub sides: Sides,
pub details: Details,
}
impl Market {
#[must_use]
pub fn headline(&self) -> String {
match &self.question_title {
Some(question) if question != &self.title => format!("{question} / {}", self.title),
_ => self.title.clone(),
}
}
#[must_use]
pub fn matches(&self, needle: &str) -> bool {
contains(&self.title, needle)
|| contains(&self.description, needle)
|| contains(self.kind.slug(), needle)
|| self.market.matches(needle)
|| self.venue.as_deref().is_some_and(|venue| contains(venue, needle))
|| self.question_title.as_deref().is_some_and(|question| contains(question, needle))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Question {
pub question: u32,
pub kind: Family,
pub title: String,
pub template: String,
pub description: String,
pub status: Status,
pub expiry: Option<Stamp>,
pub venue: Option<String>,
pub legs: Vec<Coin>,
pub settled_legs: usize,
pub fallback: Option<Coin>,
pub details: Details,
}
#[must_use]
pub fn contains(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
let (hay, needle) = (haystack.as_bytes(), needle.as_bytes());
needle.len() <= hay.len()
&& hay.windows(needle.len()).any(|window| window.iter().zip(needle).all(|(a, b)| a.eq_ignore_ascii_case(b)))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Catalog {
pub markets: Vec<Market>,
pub questions: Vec<Question>,
by_outcome: BTreeMap<u32, usize>,
by_question: BTreeMap<u32, usize>,
}
struct Owner {
id: u32,
title: String,
expiry: Option<Stamp>,
leg_titles: Vec<String>,
}
impl Catalog {
#[must_use]
pub fn build(meta: &OutcomeMeta, now_ms: u64) -> Catalog {
Catalog::build_with(meta, &Patterns::new(), now_ms)
}
#[must_use]
pub fn build_with(meta: &OutcomeMeta, patterns: &Patterns, now_ms: u64) -> Catalog {
let by_id: BTreeMap<u32, &RawOutcome> = meta.outcomes.iter().map(|raw| (raw.outcome, raw)).collect();
let mut questions = Vec::with_capacity(meta.questions.len());
let mut owners: BTreeMap<u32, Owner> = BTreeMap::new();
for raw in &meta.questions {
let question = build_question(raw, &by_id, patterns, now_ms);
let leg_titles = bucket_titles(&Fields::parse(&raw.description));
for leg in raw.named_outcomes.iter().chain(raw.fallback_outcome.iter()) {
owners.insert(
*leg,
Owner {
id: raw.question,
title: question.title.clone(),
expiry: question.expiry,
leg_titles: leg_titles.clone(),
},
);
}
questions.push(question);
}
let mut markets: Vec<Market> = meta
.outcomes
.iter()
.filter(|raw| raw.side_specs.len() == 2)
.map(|raw| build_market(raw, owners.get(&raw.outcome), patterns, now_ms))
.collect();
markets.sort_by_key(|market| (market.expiry.is_none(), market.expiry, market.kind, market.market));
questions.sort_by_key(|question| (question.expiry.is_none(), question.expiry, question.question));
Catalog::of(markets, questions)
}
#[must_use]
pub fn of(markets: Vec<Market>, questions: Vec<Question>) -> Catalog {
let mut catalog = Catalog { markets, questions, ..Catalog::default() };
catalog.reindex();
catalog
}
fn reindex(&mut self) {
self.by_outcome = self.markets.iter().enumerate().map(|(at, market)| (market.outcome, at)).collect();
self.by_question = self.questions.iter().enumerate().map(|(at, question)| (question.question, at)).collect();
}
pub fn retain_venue(&mut self, venue: &str) {
self.markets.retain(|market| market.venue.as_deref() == Some(venue));
self.questions.retain(|question| question.venue.as_deref() == Some(venue));
self.reindex();
}
pub fn retain_kinds(&mut self, kinds: &[Family]) {
self.markets.retain(|market| kinds.contains(&market.kind));
self.questions.retain(|question| kinds.contains(&question.kind));
self.reindex();
}
#[must_use]
pub fn find(&self, coin: Coin) -> Option<&Market> {
self.markets.get(self.seat(coin)?)
}
#[must_use]
pub fn seat(&self, coin: Coin) -> Option<usize> {
self.by_outcome.get(&coin.outcome()).copied()
}
#[must_use]
pub fn place_of(&self, question: u32) -> Option<usize> {
self.by_question.get(&question).copied()
}
#[must_use]
pub fn question(&self, question: u32) -> Option<&Question> {
self.questions.get(self.place_of(question)?)
}
}
fn read(
family: Family,
patterns: &Patterns,
fields: &Fields<'_>,
description: &str,
name: &str,
) -> (Option<Stamp>, Details, String) {
match family {
Family::Template => {
let id = name.strip_prefix("template:").unwrap_or(name);
let pattern: Option<&Pattern> = patterns.get(id);
from_pattern(pattern, fields, id)
}
family => interpret(family, fields, description, name),
}
}
fn build_market(raw: &RawOutcome, owner: Option<&Owner>, patterns: &Patterns, now_ms: u64) -> Market {
let template = Template::classify(&raw.name);
let fields = Fields::parse(&raw.description);
let (own_expiry, details, title) = read(template.family, patterns, &fields, &raw.description, &raw.name);
let expiry = own_expiry.or_else(|| owner.and_then(|owner| owner.expiry));
let title = fields
.seconds("index")
.and_then(|index| owner?.leg_titles.get(usize::try_from(index).ok()?).cloned())
.unwrap_or(title);
let side = |at: usize| raw.side_specs.get(at).map(|side| side.name.as_str());
Market {
market: Coin::new(raw.outcome, OutcomeSide::Yes),
outcome: raw.outcome,
kind: template.family,
title,
template: raw.name.clone(),
template_version: template.version,
description: raw.description.clone(),
status: Status::at(expiry, now_ms),
expiry,
venue: raw.venue.clone(),
quote: raw.quote_token.clone(),
fee_scale: raw.deployer_fee_scale,
question: owner.map(|owner| owner.id),
question_title: owner.map(|owner| owner.title.clone()),
sides: Sides {
yes: Coin::new(raw.outcome, OutcomeSide::Yes),
no: Coin::new(raw.outcome, OutcomeSide::No),
yes_label: label(side(0), &fields, "Yes"),
no_label: label(side(1), &fields, "No"),
},
details,
}
}
fn build_question(raw: &RawQuestion, by_id: &BTreeMap<u32, &RawOutcome>, patterns: &Patterns, now_ms: u64) -> Question {
let template = Template::classify(&raw.name);
let fields = Fields::parse(&raw.description);
let (expiry, details, title) = read(template.family, patterns, &fields, &raw.description, &raw.name);
let venue = raw
.named_outcomes
.iter()
.chain(raw.fallback_outcome.iter())
.find_map(|id| by_id.get(id).and_then(|outcome| outcome.venue.clone()));
let listed = |id: &u32| by_id.contains_key(id);
Question {
question: raw.question,
kind: template.family,
title,
template: raw.name.clone(),
description: raw.description.clone(),
status: Status::at(expiry, now_ms),
expiry,
venue,
legs: raw.named_outcomes.iter().filter(|id| listed(id)).map(|id| Coin::new(*id, OutcomeSide::Yes)).collect(),
settled_legs: raw.settled_named_outcomes.len(),
fallback: raw.fallback_outcome.filter(listed).map(|id| Coin::new(id, OutcomeSide::Yes)),
details,
}
}
#[cfg(test)]
#[path = "tests/market.rs"]
mod tests;