@ebowwa/quant-rust 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/bun-ffi.d.ts +54 -0
- package/dist/index.js +576 -0
- package/dist/src/index.d.ts +324 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.d.ts.map +1 -0
- package/native/README.md +62 -0
- package/native/darwin-arm64/libquant_rust.dylib +0 -0
- package/package.json +70 -0
- package/scripts/postinstall.cjs +85 -0
- package/src/ffi.rs +496 -0
- package/src/index.ts +1073 -0
- package/src/indicators/ma.rs +222 -0
- package/src/indicators/mod.rs +18 -0
- package/src/indicators/momentum.rs +353 -0
- package/src/indicators/sr.rs +195 -0
- package/src/indicators/trend.rs +351 -0
- package/src/indicators/volatility.rs +270 -0
- package/src/indicators/volume.rs +213 -0
- package/src/lib.rs +130 -0
- package/src/patterns/breakout.rs +431 -0
- package/src/patterns/chart.rs +772 -0
- package/src/patterns/mod.rs +394 -0
- package/src/patterns/sr.rs +423 -0
- package/src/prediction/amm.rs +338 -0
- package/src/prediction/arbitrage.rs +230 -0
- package/src/prediction/calibration.rs +317 -0
- package/src/prediction/kelly.rs +232 -0
- package/src/prediction/lmsr.rs +194 -0
- package/src/prediction/mod.rs +59 -0
- package/src/prediction/odds.rs +229 -0
- package/src/prediction/pnl.rs +254 -0
- package/src/prediction/risk.rs +228 -0
- package/src/risk/beta.rs +257 -0
- package/src/risk/drawdown.rs +256 -0
- package/src/risk/leverage.rs +201 -0
- package/src/risk/mod.rs +388 -0
- package/src/risk/portfolio.rs +287 -0
- package/src/risk/ratios.rs +290 -0
- package/src/risk/sizing.rs +194 -0
- package/src/risk/var.rs +222 -0
- package/src/stats/cdf.rs +257 -0
- package/src/stats/correlation.rs +225 -0
- package/src/stats/distribution.rs +194 -0
- package/src/stats/hypothesis.rs +177 -0
- package/src/stats/matrix.rs +346 -0
- package/src/stats/mod.rs +257 -0
- package/src/stats/regression.rs +239 -0
- package/src/stats/rolling.rs +193 -0
- package/src/stats/timeseries.rs +263 -0
- package/src/types.rs +224 -0
- package/src/utils/mod.rs +215 -0
- package/src/utils/normalize.rs +192 -0
- package/src/utils/price.rs +167 -0
- package/src/utils/quantiles.rs +177 -0
- package/src/utils/returns.rs +158 -0
- package/src/utils/rolling.rs +97 -0
- package/src/utils/stats.rs +154 -0
- package/types/index.ts +513 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//! # LMSR (Logarithmic Market Scoring Rule)
|
|
2
|
+
//!
|
|
3
|
+
//! Calculations for LMSR-based prediction markets (used by Gnosis, Hanson, etc.)
|
|
4
|
+
|
|
5
|
+
use crate::prediction::Outcome;
|
|
6
|
+
|
|
7
|
+
/// State of an LMSR market
|
|
8
|
+
#[derive(Debug, Clone, Copy)]
|
|
9
|
+
pub struct LMSRState {
|
|
10
|
+
/// YES shares outstanding
|
|
11
|
+
pub yes_shares: f64,
|
|
12
|
+
/// NO shares outstanding
|
|
13
|
+
pub no_shares: f64,
|
|
14
|
+
/// Liquidity parameter (b)
|
|
15
|
+
pub b: f64,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl LMSRState {
|
|
19
|
+
/// Create a new LMSR state
|
|
20
|
+
pub fn new(yes_shares: f64, no_shares: f64, b: f64) -> Self {
|
|
21
|
+
Self {
|
|
22
|
+
yes_shares,
|
|
23
|
+
no_shares,
|
|
24
|
+
b,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Calculate price in LMSR market.
|
|
30
|
+
///
|
|
31
|
+
/// Price = e^(q_i / b) / sum(e^(q_j / b))
|
|
32
|
+
///
|
|
33
|
+
/// # Arguments
|
|
34
|
+
/// * `state` - Current LMSR state
|
|
35
|
+
/// * `outcome` - Which outcome to price
|
|
36
|
+
///
|
|
37
|
+
/// # Returns
|
|
38
|
+
/// Price as probability (0-1)
|
|
39
|
+
///
|
|
40
|
+
/// # Example
|
|
41
|
+
/// ```
|
|
42
|
+
/// use quant_rust::prediction::lmsr::{LMSRState, lmsr_price};
|
|
43
|
+
/// use quant_rust::prediction::Outcome;
|
|
44
|
+
/// let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
45
|
+
/// let price = lmsr_price(&state, Outcome::Yes);
|
|
46
|
+
/// assert!((price - 0.5).abs() < 1e-10); // Equal shares = 50/50
|
|
47
|
+
/// ```
|
|
48
|
+
pub fn lmsr_price(state: &LMSRState, outcome: Outcome) -> f64 {
|
|
49
|
+
let LMSRState { yes_shares, no_shares, b } = *state;
|
|
50
|
+
|
|
51
|
+
if b <= 0.0 {
|
|
52
|
+
return 0.0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let exp_yes = (yes_shares / b).exp();
|
|
56
|
+
let exp_no = (no_shares / b).exp();
|
|
57
|
+
let sum = exp_yes + exp_no;
|
|
58
|
+
|
|
59
|
+
if sum == 0.0 {
|
|
60
|
+
return 0.5;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
match outcome {
|
|
64
|
+
Outcome::Yes => exp_yes / sum,
|
|
65
|
+
Outcome::No => exp_no / sum,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Calculate cost to buy shares in LMSR.
|
|
70
|
+
///
|
|
71
|
+
/// Cost = b * ln(sum(e^(q_i + delta_i) / b)) - b * ln(sum(e^(q_i) / b))
|
|
72
|
+
///
|
|
73
|
+
/// # Arguments
|
|
74
|
+
/// * `state` - Current LMSR state
|
|
75
|
+
/// * `outcome` - Which outcome to buy
|
|
76
|
+
/// * `shares` - Number of shares to buy
|
|
77
|
+
///
|
|
78
|
+
/// # Returns
|
|
79
|
+
/// Cost in quote currency
|
|
80
|
+
///
|
|
81
|
+
/// # Example
|
|
82
|
+
/// ```
|
|
83
|
+
/// use quant_rust::prediction::lmsr::{LMSRState, lmsr_buy_cost};
|
|
84
|
+
/// use quant_rust::prediction::Outcome;
|
|
85
|
+
/// let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
86
|
+
/// let cost = lmsr_buy_cost(&state, Outcome::Yes, 50.0);
|
|
87
|
+
/// assert!(cost > 0.0);
|
|
88
|
+
/// ```
|
|
89
|
+
pub fn lmsr_buy_cost(state: &LMSRState, outcome: Outcome, shares: f64) -> f64 {
|
|
90
|
+
let LMSRState { yes_shares, no_shares, b } = *state;
|
|
91
|
+
|
|
92
|
+
if b <= 0.0 {
|
|
93
|
+
return 0.0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Before cost: b * ln(e^(yes/b) + e^(no/b))
|
|
97
|
+
let before = b * ((yes_shares / b).exp() + (no_shares / b).exp()).ln();
|
|
98
|
+
|
|
99
|
+
// After cost depends on which outcome we're buying
|
|
100
|
+
let after = match outcome {
|
|
101
|
+
Outcome::Yes => {
|
|
102
|
+
b * (((yes_shares + shares) / b).exp() + (no_shares / b).exp()).ln()
|
|
103
|
+
}
|
|
104
|
+
Outcome::No => {
|
|
105
|
+
b * ((yes_shares / b).exp() + ((no_shares + shares) / b).exp()).ln()
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
after - before
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[cfg(test)]
|
|
113
|
+
mod tests {
|
|
114
|
+
use super::*;
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_lmsr_price_equal() {
|
|
118
|
+
// Equal shares should give 50/50
|
|
119
|
+
let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
120
|
+
let yes_price = lmsr_price(&state, Outcome::Yes);
|
|
121
|
+
let no_price = lmsr_price(&state, Outcome::No);
|
|
122
|
+
assert!((yes_price - 0.5).abs() < 1e-10);
|
|
123
|
+
assert!((no_price - 0.5).abs() < 1e-10);
|
|
124
|
+
assert!((yes_price + no_price - 1.0).abs() < 1e-10);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn test_lmsr_price_more_yes() {
|
|
129
|
+
// More YES shares should increase YES price
|
|
130
|
+
let state = LMSRState::new(100.0, 0.0, 100.0);
|
|
131
|
+
let yes_price = lmsr_price(&state, Outcome::Yes);
|
|
132
|
+
assert!(yes_price > 0.5);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn test_lmsr_price_more_no() {
|
|
137
|
+
// More NO shares should increase NO price
|
|
138
|
+
let state = LMSRState::new(0.0, 100.0, 100.0);
|
|
139
|
+
let yes_price = lmsr_price(&state, Outcome::Yes);
|
|
140
|
+
assert!(yes_price < 0.5);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn test_lmsr_buy_cost_positive() {
|
|
145
|
+
let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
146
|
+
let cost = lmsr_buy_cost(&state, Outcome::Yes, 50.0);
|
|
147
|
+
assert!(cost > 0.0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn test_lmsr_buy_cost_increases_with_shares() {
|
|
152
|
+
let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
153
|
+
let cost_small = lmsr_buy_cost(&state, Outcome::Yes, 10.0);
|
|
154
|
+
let cost_large = lmsr_buy_cost(&state, Outcome::Yes, 50.0);
|
|
155
|
+
assert!(cost_large > cost_small);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn test_lmsr_buy_cost_symmetry() {
|
|
160
|
+
let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
161
|
+
let cost_yes = lmsr_buy_cost(&state, Outcome::Yes, 50.0);
|
|
162
|
+
let cost_no = lmsr_buy_cost(&state, Outcome::No, 50.0);
|
|
163
|
+
// Should cost the same to buy either outcome from equal starting point
|
|
164
|
+
assert!((cost_yes - cost_no).abs() < 1e-10);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn test_lmsr_buy_cost_zero_shares() {
|
|
169
|
+
let state = LMSRState::new(0.0, 0.0, 100.0);
|
|
170
|
+
let cost = lmsr_buy_cost(&state, Outcome::Yes, 0.0);
|
|
171
|
+
assert!((cost - 0.0).abs() < 1e-10);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn test_lmsr_liquidity_parameter_effect() {
|
|
176
|
+
// Higher b = more liquidity = prices move less
|
|
177
|
+
let state_low = LMSRState::new(100.0, 0.0, 50.0);
|
|
178
|
+
let state_high = LMSRState::new(100.0, 0.0, 200.0);
|
|
179
|
+
|
|
180
|
+
let price_low = lmsr_price(&state_low, Outcome::Yes);
|
|
181
|
+
let price_high = lmsr_price(&state_high, Outcome::Yes);
|
|
182
|
+
|
|
183
|
+
// With same shares but higher b, price should be closer to 0.5
|
|
184
|
+
assert!(price_high < price_low);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[test]
|
|
188
|
+
fn test_lmsr_prices_sum_to_one() {
|
|
189
|
+
let state = LMSRState::new(123.0, 456.0, 100.0);
|
|
190
|
+
let yes_price = lmsr_price(&state, Outcome::Yes);
|
|
191
|
+
let no_price = lmsr_price(&state, Outcome::No);
|
|
192
|
+
assert!((yes_price + no_price - 1.0).abs() < 1e-10);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//! # Prediction Market Tools
|
|
2
|
+
//!
|
|
3
|
+
//! Tools for binary outcome prediction markets (Polymarket, Manifold, Kalshi, etc.)
|
|
4
|
+
//!
|
|
5
|
+
//! ## Modules
|
|
6
|
+
//!
|
|
7
|
+
//! - [`odds`] - Probability and odds conversions
|
|
8
|
+
//! - [`kelly`] - Kelly criterion position sizing
|
|
9
|
+
//! - [`amm`] - Constant product AMM calculations
|
|
10
|
+
//! - [`lmsr`] - Logarithmic Market Scoring Rule
|
|
11
|
+
//! - [`arbitrage`] - Arbitrage detection
|
|
12
|
+
//! - [`calibration`] - Forecast calibration and scoring
|
|
13
|
+
//! - [`risk`] - Position risk metrics
|
|
14
|
+
//! - [`pnl`] - PnL and payout calculations
|
|
15
|
+
|
|
16
|
+
pub mod amm;
|
|
17
|
+
pub mod arbitrage;
|
|
18
|
+
pub mod calibration;
|
|
19
|
+
pub mod kelly;
|
|
20
|
+
pub mod lmsr;
|
|
21
|
+
pub mod odds;
|
|
22
|
+
pub mod pnl;
|
|
23
|
+
pub mod risk;
|
|
24
|
+
|
|
25
|
+
// Re-exports for convenience
|
|
26
|
+
pub use amm::{
|
|
27
|
+
amm_buy_cost, amm_impermanent_loss, amm_price_after_buy, amm_price_impact, amm_shares_received,
|
|
28
|
+
AMMState, PriceImpactResult,
|
|
29
|
+
};
|
|
30
|
+
pub use arbitrage::{
|
|
31
|
+
detect_arbitrage, find_cross_market_arbitrage, ArbitrageOpportunity, CrossMarketArbitrage,
|
|
32
|
+
MarketPrice,
|
|
33
|
+
};
|
|
34
|
+
pub use calibration::{
|
|
35
|
+
brier_score, calculate_calibration, log_loss, CalibrationBucket, CalibrationResult, Prediction,
|
|
36
|
+
};
|
|
37
|
+
pub use kelly::{fractional_kelly, kelly_criterion, kelly_multiple, BetOpportunity, KellyResult};
|
|
38
|
+
pub use lmsr::{lmsr_buy_cost, lmsr_price, LMSRState};
|
|
39
|
+
pub use odds::{
|
|
40
|
+
american_odds_to_prob, decimal_odds_to_prob, price_to_implied_prob, prob_to_american_odds,
|
|
41
|
+
prob_to_decimal_odds, prob_to_fair_price,
|
|
42
|
+
};
|
|
43
|
+
pub use pnl::{
|
|
44
|
+
calculate_payout, calculate_pnl, calculate_unrealized_pnl, time_weighted_return,
|
|
45
|
+
ClosedPosition,
|
|
46
|
+
};
|
|
47
|
+
pub use risk::{
|
|
48
|
+
break_even_prob, calculate_edge, calculate_prediction_risk, has_positive_ev,
|
|
49
|
+
PredictionRiskMetrics,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
use serde::{Deserialize, Serialize};
|
|
53
|
+
|
|
54
|
+
/// Outcome type for prediction markets
|
|
55
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
56
|
+
pub enum Outcome {
|
|
57
|
+
Yes,
|
|
58
|
+
No,
|
|
59
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//! # Odds and Probability Conversions
|
|
2
|
+
//!
|
|
3
|
+
//! Convert between probability, decimal odds, and American odds formats.
|
|
4
|
+
|
|
5
|
+
use std::f64::INFINITY;
|
|
6
|
+
use std::f64::NEG_INFINITY;
|
|
7
|
+
|
|
8
|
+
/// Convert probability (0-1) to decimal odds.
|
|
9
|
+
///
|
|
10
|
+
/// # Arguments
|
|
11
|
+
/// * `prob` - Probability value (0-1)
|
|
12
|
+
///
|
|
13
|
+
/// # Returns
|
|
14
|
+
/// Decimal odds (1 / prob), or infinity if prob is 0 or negative
|
|
15
|
+
///
|
|
16
|
+
/// # Example
|
|
17
|
+
/// ```
|
|
18
|
+
/// use quant_rust::prediction::odds::prob_to_decimal_odds;
|
|
19
|
+
/// let odds = prob_to_decimal_odds(0.25);
|
|
20
|
+
/// assert_eq!(odds, 4.0);
|
|
21
|
+
/// ```
|
|
22
|
+
pub fn prob_to_decimal_odds(prob: f64) -> f64 {
|
|
23
|
+
if prob <= 0.0 {
|
|
24
|
+
return INFINITY;
|
|
25
|
+
}
|
|
26
|
+
1.0 / prob
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Convert probability (0-1) to American odds.
|
|
30
|
+
///
|
|
31
|
+
/// # Arguments
|
|
32
|
+
/// * `prob` - Probability value (0-1)
|
|
33
|
+
///
|
|
34
|
+
/// # Returns
|
|
35
|
+
/// American odds:
|
|
36
|
+
/// - Positive (>100): underdog, profit on $100 bet
|
|
37
|
+
/// - Negative (<-100): favorite, amount to bet to win $100
|
|
38
|
+
///
|
|
39
|
+
/// # Example
|
|
40
|
+
/// ```
|
|
41
|
+
/// use quant_rust::prediction::odds::prob_to_american_odds;
|
|
42
|
+
/// let odds = prob_to_american_odds(0.25);
|
|
43
|
+
/// assert_eq!(odds, 300); // +300 underdog
|
|
44
|
+
///
|
|
45
|
+
/// let odds = prob_to_american_odds(0.75);
|
|
46
|
+
/// assert_eq!(odds, -300); // -300 favorite
|
|
47
|
+
/// ```
|
|
48
|
+
pub fn prob_to_american_odds(prob: f64) -> f64 {
|
|
49
|
+
if prob <= 0.0 {
|
|
50
|
+
return INFINITY;
|
|
51
|
+
}
|
|
52
|
+
if prob >= 1.0 {
|
|
53
|
+
return NEG_INFINITY;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if prob > 0.5 {
|
|
57
|
+
// Favorite: negative odds
|
|
58
|
+
-((prob / (1.0 - prob)) * 100.0).round()
|
|
59
|
+
} else {
|
|
60
|
+
// Underdog: positive odds
|
|
61
|
+
(((1.0 - prob) / prob) * 100.0).round()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Convert decimal odds to probability.
|
|
66
|
+
///
|
|
67
|
+
/// # Arguments
|
|
68
|
+
/// * `odds` - Decimal odds (must be >= 1)
|
|
69
|
+
///
|
|
70
|
+
/// # Returns
|
|
71
|
+
/// Implied probability (1 / odds), or 1.0 if odds <= 1
|
|
72
|
+
///
|
|
73
|
+
/// # Example
|
|
74
|
+
/// ```
|
|
75
|
+
/// use quant_rust::prediction::odds::decimal_odds_to_prob;
|
|
76
|
+
/// let prob = decimal_odds_to_prob(4.0);
|
|
77
|
+
/// assert_eq!(prob, 0.25);
|
|
78
|
+
/// ```
|
|
79
|
+
pub fn decimal_odds_to_prob(odds: f64) -> f64 {
|
|
80
|
+
if odds <= 1.0 {
|
|
81
|
+
return 1.0;
|
|
82
|
+
}
|
|
83
|
+
1.0 / odds
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Convert American odds to probability.
|
|
87
|
+
///
|
|
88
|
+
/// # Arguments
|
|
89
|
+
/// * `odds` - American odds (positive or negative)
|
|
90
|
+
///
|
|
91
|
+
/// # Returns
|
|
92
|
+
/// Implied probability
|
|
93
|
+
///
|
|
94
|
+
/// # Example
|
|
95
|
+
/// ```
|
|
96
|
+
/// use quant_rust::prediction::odds::american_odds_to_prob;
|
|
97
|
+
/// let prob = american_odds_to_prob(300.0);
|
|
98
|
+
/// assert_eq!(prob, 0.25);
|
|
99
|
+
///
|
|
100
|
+
/// let prob = american_odds_to_prob(-300.0);
|
|
101
|
+
/// assert_eq!(prob, 0.75);
|
|
102
|
+
/// ```
|
|
103
|
+
pub fn american_odds_to_prob(odds: f64) -> f64 {
|
|
104
|
+
if odds > 0.0 {
|
|
105
|
+
// Underdog: prob = 100 / (odds + 100)
|
|
106
|
+
100.0 / (odds + 100.0)
|
|
107
|
+
} else {
|
|
108
|
+
// Favorite: prob = |odds| / (|odds| + 100)
|
|
109
|
+
odds.abs() / (odds.abs() + 100.0)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Convert price (0-1) to implied probability, accounting for fees.
|
|
114
|
+
///
|
|
115
|
+
/// # Arguments
|
|
116
|
+
/// * `price` - Market price (0-1)
|
|
117
|
+
/// * `fee_bps` - Optional fee in basis points (e.g., 100 = 1%)
|
|
118
|
+
///
|
|
119
|
+
/// # Returns
|
|
120
|
+
/// Implied probability including fee impact
|
|
121
|
+
///
|
|
122
|
+
/// # Example
|
|
123
|
+
/// ```
|
|
124
|
+
/// use quant_rust::prediction::odds::price_to_implied_prob;
|
|
125
|
+
/// let prob = price_to_implied_prob(0.50, Some(100.0)); // 1% fee
|
|
126
|
+
/// assert!((prob - 0.505).abs() < 0.001);
|
|
127
|
+
/// ```
|
|
128
|
+
pub fn price_to_implied_prob(price: f64, fee_bps: Option<f64>) -> f64 {
|
|
129
|
+
if price <= 0.0 {
|
|
130
|
+
return 0.0;
|
|
131
|
+
}
|
|
132
|
+
if price >= 1.0 {
|
|
133
|
+
return 1.0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let fee = fee_bps.unwrap_or(0.0);
|
|
137
|
+
price * (1.0 + fee / 10000.0)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Convert probability to fair price, accounting for fees.
|
|
141
|
+
///
|
|
142
|
+
/// # Arguments
|
|
143
|
+
/// * `prob` - Probability value (0-1)
|
|
144
|
+
/// * `fee_bps` - Optional fee in basis points (e.g., 100 = 1%)
|
|
145
|
+
///
|
|
146
|
+
/// # Returns
|
|
147
|
+
/// Fair price after accounting for fees
|
|
148
|
+
///
|
|
149
|
+
/// # Example
|
|
150
|
+
/// ```
|
|
151
|
+
/// use quant_rust::prediction::odds::prob_to_fair_price;
|
|
152
|
+
/// let price = prob_to_fair_price(0.50, Some(100.0)); // 1% fee
|
|
153
|
+
/// assert!((price - 0.495).abs() < 0.001);
|
|
154
|
+
/// ```
|
|
155
|
+
pub fn prob_to_fair_price(prob: f64, fee_bps: Option<f64>) -> f64 {
|
|
156
|
+
if prob <= 0.0 {
|
|
157
|
+
return 0.0;
|
|
158
|
+
}
|
|
159
|
+
if prob >= 1.0 {
|
|
160
|
+
return 1.0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let fee = fee_bps.unwrap_or(0.0);
|
|
164
|
+
prob / (1.0 + fee / 10000.0)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[cfg(test)]
|
|
168
|
+
mod tests {
|
|
169
|
+
use super::*;
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_prob_to_decimal_odds() {
|
|
173
|
+
assert_eq!(prob_to_decimal_odds(0.25), 4.0);
|
|
174
|
+
assert_eq!(prob_to_decimal_odds(0.5), 2.0);
|
|
175
|
+
assert_eq!(prob_to_decimal_odds(1.0), 1.0);
|
|
176
|
+
assert!(prob_to_decimal_odds(0.0).is_infinite());
|
|
177
|
+
assert!(prob_to_decimal_odds(-0.1).is_infinite());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn test_prob_to_american_odds() {
|
|
182
|
+
assert_eq!(prob_to_american_odds(0.25), 300.0); // +300 underdog
|
|
183
|
+
assert_eq!(prob_to_american_odds(0.75), -300.0); // -300 favorite
|
|
184
|
+
assert_eq!(prob_to_american_odds(0.5), 100.0); // Even money
|
|
185
|
+
assert!(prob_to_american_odds(0.0).is_infinite());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn test_decimal_odds_to_prob() {
|
|
190
|
+
assert_eq!(decimal_odds_to_prob(4.0), 0.25);
|
|
191
|
+
assert_eq!(decimal_odds_to_prob(2.0), 0.5);
|
|
192
|
+
assert_eq!(decimal_odds_to_prob(1.0), 1.0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn test_american_odds_to_prob() {
|
|
197
|
+
assert_eq!(american_odds_to_prob(300.0), 0.25);
|
|
198
|
+
assert_eq!(american_odds_to_prob(-300.0), 0.75);
|
|
199
|
+
assert_eq!(american_odds_to_prob(100.0), 0.5);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn test_price_to_implied_prob() {
|
|
204
|
+
assert_eq!(price_to_implied_prob(0.5, None), 0.5);
|
|
205
|
+
assert_eq!(price_to_implied_prob(0.5, Some(0.0)), 0.5);
|
|
206
|
+
assert!((price_to_implied_prob(0.5, Some(100.0)) - 0.505).abs() < 0.001);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn test_prob_to_fair_price() {
|
|
211
|
+
assert_eq!(prob_to_fair_price(0.5, None), 0.5);
|
|
212
|
+
assert_eq!(prob_to_fair_price(0.5, Some(0.0)), 0.5);
|
|
213
|
+
assert!((prob_to_fair_price(0.5, Some(100.0)) - 0.49505).abs() < 0.001);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn test_round_trip_conversion() {
|
|
218
|
+
// Decimal odds round trip
|
|
219
|
+
let prob = 0.4;
|
|
220
|
+
let odds = prob_to_decimal_odds(prob);
|
|
221
|
+
let back = decimal_odds_to_prob(odds);
|
|
222
|
+
assert!((prob - back).abs() < 1e-10);
|
|
223
|
+
|
|
224
|
+
// American odds round trip
|
|
225
|
+
let odds = prob_to_american_odds(prob);
|
|
226
|
+
let back = american_odds_to_prob(odds);
|
|
227
|
+
assert!((prob - back).abs() < 0.01); // Allow rounding error
|
|
228
|
+
}
|
|
229
|
+
}
|