kalshi-backtest/src/types.rs
Nicholai 025322219c feat: initial commit with code quality refactoring
kalshi prediction market backtesting framework with:
- trading pipeline (sources, filters, scorers, selectors)
- position sizing with kelly criterion
- multiple scoring strategies (momentum, mean reversion, etc)
- random baseline for comparison

refactoring includes:
- extract shared resolve_closed_positions() function
- reduce RandomBaseline::run() nesting with helper functions
- move MarketCandidate Default impl to types.rs
- add explanatory comments to complex logic
2026-01-21 09:32:12 -07:00

344 lines
8.9 KiB
Rust

use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Side {
Yes,
No,
}
impl Side {
pub fn opposite(&self) -> Self {
match self {
Side::Yes => Side::No,
Side::No => Side::Yes,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MarketResult {
Yes,
No,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketCandidate {
pub ticker: String,
pub title: String,
pub category: String,
pub current_yes_price: Decimal,
pub current_no_price: Decimal,
pub volume_24h: u64,
pub total_volume: u64,
pub buy_volume_24h: u64,
pub sell_volume_24h: u64,
pub open_time: DateTime<Utc>,
pub close_time: DateTime<Utc>,
pub result: Option<MarketResult>,
pub price_history: Vec<PricePoint>,
pub scores: HashMap<String, f64>,
pub final_score: f64,
}
impl MarketCandidate {
pub fn time_to_close(&self, now: DateTime<Utc>) -> chrono::Duration {
self.close_time - now
}
pub fn is_open(&self, now: DateTime<Utc>) -> bool {
now >= self.open_time && now < self.close_time
}
}
impl Default for MarketCandidate {
fn default() -> Self {
Self {
ticker: String::new(),
title: String::new(),
category: String::new(),
current_yes_price: Decimal::ZERO,
current_no_price: Decimal::ZERO,
volume_24h: 0,
total_volume: 0,
buy_volume_24h: 0,
sell_volume_24h: 0,
open_time: Utc::now(),
close_time: Utc::now(),
result: None,
price_history: Vec::new(),
scores: HashMap::new(),
final_score: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricePoint {
pub timestamp: DateTime<Utc>,
pub yes_price: Decimal,
pub volume: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradingContext {
pub request_id: String,
pub timestamp: DateTime<Utc>,
pub portfolio: Portfolio,
pub trading_history: Vec<Trade>,
}
impl TradingContext {
pub fn new(initial_capital: Decimal, start_time: DateTime<Utc>) -> Self {
Self {
request_id: uuid::Uuid::new_v4().to_string(),
timestamp: start_time,
portfolio: Portfolio::new(initial_capital),
trading_history: Vec::new(),
}
}
pub fn request_id(&self) -> &str {
&self.request_id
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Portfolio {
pub positions: HashMap<String, Position>,
pub cash: Decimal,
pub initial_capital: Decimal,
}
impl Portfolio {
pub fn new(initial_capital: Decimal) -> Self {
Self {
positions: HashMap::new(),
cash: initial_capital,
initial_capital,
}
}
pub fn total_value(&self, market_prices: &HashMap<String, Decimal>) -> Decimal {
let position_value: Decimal = self
.positions
.values()
.map(|p| {
let price = market_prices.get(&p.ticker).copied().unwrap_or(p.avg_entry_price);
let effective_price = match p.side {
Side::Yes => price,
Side::No => Decimal::ONE - price,
};
effective_price * Decimal::from(p.quantity)
})
.sum();
self.cash + position_value
}
pub fn has_position(&self, ticker: &str) -> bool {
self.positions.contains_key(ticker)
}
pub fn get_position(&self, ticker: &str) -> Option<&Position> {
self.positions.get(ticker)
}
pub fn apply_fill(&mut self, fill: &Fill) {
let cost = fill.price * Decimal::from(fill.quantity);
match fill.side {
Side::Yes | Side::No => {
self.cash -= cost;
let position = self.positions.entry(fill.ticker.clone()).or_insert_with(|| {
Position {
ticker: fill.ticker.clone(),
side: fill.side,
quantity: 0,
avg_entry_price: Decimal::ZERO,
entry_time: fill.timestamp,
}
});
let total_cost =
position.avg_entry_price * Decimal::from(position.quantity) + cost;
position.quantity += fill.quantity;
if position.quantity > 0 {
position.avg_entry_price = total_cost / Decimal::from(position.quantity);
}
}
}
}
pub fn resolve_position(&mut self, ticker: &str, result: MarketResult) -> Option<Decimal> {
let position = self.positions.remove(ticker)?;
let payout = match (result, position.side) {
(MarketResult::Yes, Side::Yes) | (MarketResult::No, Side::No) => {
Decimal::from(position.quantity)
}
(MarketResult::Cancelled, _) => {
position.avg_entry_price * Decimal::from(position.quantity)
}
_ => Decimal::ZERO,
};
self.cash += payout;
let cost = position.avg_entry_price * Decimal::from(position.quantity);
Some(payout - cost)
}
pub fn close_position(&mut self, ticker: &str, exit_price: Decimal) -> Option<Decimal> {
let position = self.positions.remove(ticker)?;
let effective_exit_price = match position.side {
Side::Yes => exit_price,
Side::No => Decimal::ONE - exit_price,
};
let exit_value = effective_exit_price * Decimal::from(position.quantity);
self.cash += exit_value;
let cost = position.avg_entry_price * Decimal::from(position.quantity);
Some(exit_value - cost)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Position {
pub ticker: String,
pub side: Side,
pub quantity: u64,
pub avg_entry_price: Decimal,
pub entry_time: DateTime<Utc>,
}
impl Position {
pub fn cost_basis(&self) -> Decimal {
self.avg_entry_price * Decimal::from(self.quantity)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trade {
pub ticker: String,
pub side: Side,
pub quantity: u64,
pub price: Decimal,
pub timestamp: DateTime<Utc>,
pub trade_type: TradeType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TradeType {
Open,
Close,
Resolution,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ExitReason {
Resolution(MarketResult),
TakeProfit { pnl_pct: f64 },
StopLoss { pnl_pct: f64 },
TimeStop { hours_held: i64 },
ScoreReversal { new_score: f64 },
}
#[derive(Debug, Clone)]
pub struct ExitSignal {
pub ticker: String,
pub reason: ExitReason,
pub current_price: Decimal,
}
#[derive(Debug, Clone)]
pub struct ExitConfig {
pub take_profit_pct: f64,
pub stop_loss_pct: f64,
pub max_hold_hours: i64,
pub score_reversal_threshold: f64,
}
impl Default for ExitConfig {
fn default() -> Self {
Self {
take_profit_pct: 0.20,
stop_loss_pct: 0.15,
max_hold_hours: 72,
score_reversal_threshold: -0.3,
}
}
}
impl ExitConfig {
pub fn conservative() -> Self {
Self {
take_profit_pct: 0.15,
stop_loss_pct: 0.10,
max_hold_hours: 48,
score_reversal_threshold: -0.2,
}
}
pub fn aggressive() -> Self {
Self {
take_profit_pct: 0.30,
stop_loss_pct: 0.20,
max_hold_hours: 120,
score_reversal_threshold: -0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fill {
pub ticker: String,
pub side: Side,
pub quantity: u64,
pub price: Decimal,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub ticker: String,
pub side: Side,
pub quantity: u64,
pub limit_price: Option<Decimal>,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketData {
pub ticker: String,
pub title: String,
pub category: String,
pub open_time: DateTime<Utc>,
pub close_time: DateTime<Utc>,
pub result: Option<MarketResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeData {
pub timestamp: DateTime<Utc>,
pub ticker: String,
pub price: Decimal,
pub volume: u64,
pub taker_side: Side,
}
#[derive(Debug, Clone)]
pub struct BacktestConfig {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub interval: chrono::Duration,
pub initial_capital: Decimal,
pub max_position_size: u64,
pub max_positions: usize,
}