@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,338 @@
|
|
|
1
|
+
//! # AMM Calculations (Constant Product Market Maker)
|
|
2
|
+
//!
|
|
3
|
+
//! Calculations for constant product AMMs (Uniswap V2 style) used in prediction markets.
|
|
4
|
+
|
|
5
|
+
use crate::prediction::Outcome;
|
|
6
|
+
|
|
7
|
+
/// State of a constant product AMM
|
|
8
|
+
#[derive(Debug, Clone, Copy)]
|
|
9
|
+
pub struct AMMState {
|
|
10
|
+
/// Shares in YES pool
|
|
11
|
+
pub pool_yes: f64,
|
|
12
|
+
/// Shares in NO pool
|
|
13
|
+
pub pool_no: f64,
|
|
14
|
+
/// Constant product (pool_yes * pool_no)
|
|
15
|
+
pub k: f64,
|
|
16
|
+
/// Total LP tokens outstanding
|
|
17
|
+
pub lp_token_supply: f64,
|
|
18
|
+
/// Fee in basis points
|
|
19
|
+
pub fee: f64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl AMMState {
|
|
23
|
+
/// Create a new AMM state
|
|
24
|
+
pub fn new(pool_yes: f64, pool_no: f64, lp_token_supply: f64, fee: f64) -> Self {
|
|
25
|
+
let k = pool_yes * pool_no;
|
|
26
|
+
Self {
|
|
27
|
+
pool_yes,
|
|
28
|
+
pool_no,
|
|
29
|
+
k,
|
|
30
|
+
lp_token_supply,
|
|
31
|
+
fee,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Result of price impact calculation
|
|
37
|
+
#[derive(Debug, Clone, Copy)]
|
|
38
|
+
pub struct PriceImpactResult {
|
|
39
|
+
/// Current price before trade
|
|
40
|
+
pub current_price: f64,
|
|
41
|
+
/// Price after trade execution
|
|
42
|
+
pub new_price: f64,
|
|
43
|
+
/// Average price paid per share
|
|
44
|
+
pub avg_price: f64,
|
|
45
|
+
/// Price impact as percentage
|
|
46
|
+
pub price_impact: f64,
|
|
47
|
+
/// Slippage from current price
|
|
48
|
+
pub slippage: f64,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Calculate price after buying shares (constant product AMM).
|
|
52
|
+
///
|
|
53
|
+
/// Uses xy = k invariant. When buying YES, the YES price increases.
|
|
54
|
+
///
|
|
55
|
+
/// # Arguments
|
|
56
|
+
/// * `state` - Current AMM state
|
|
57
|
+
/// * `outcome` - Which outcome to buy (Yes or No)
|
|
58
|
+
/// * `shares` - Number of shares to buy
|
|
59
|
+
///
|
|
60
|
+
/// # Returns
|
|
61
|
+
/// Price after the trade
|
|
62
|
+
///
|
|
63
|
+
/// # Example
|
|
64
|
+
/// ```
|
|
65
|
+
/// use quant_rust::prediction::amm::{AMMState, amm_price_after_buy};
|
|
66
|
+
/// use quant_rust::prediction::Outcome;
|
|
67
|
+
/// let state = AMMState::new(1000.0, 1000.0, 1000.0, 0.0);
|
|
68
|
+
/// let price = amm_price_after_buy(&state, Outcome::Yes, 100.0);
|
|
69
|
+
/// assert!(price > 0.5); // Price increases after buying YES
|
|
70
|
+
/// ```
|
|
71
|
+
pub fn amm_price_after_buy(state: &AMMState, outcome: Outcome, shares: f64) -> f64 {
|
|
72
|
+
let AMMState { pool_yes, pool_no, .. } = *state;
|
|
73
|
+
|
|
74
|
+
match outcome {
|
|
75
|
+
Outcome::Yes => {
|
|
76
|
+
// Buying YES: poolNo increases (you're betting against NO)
|
|
77
|
+
// The pool balances shift to reflect higher YES probability
|
|
78
|
+
let new_pool_no = pool_no + shares * (pool_no / pool_yes);
|
|
79
|
+
new_pool_no / (pool_yes + new_pool_no)
|
|
80
|
+
}
|
|
81
|
+
Outcome::No => {
|
|
82
|
+
// Buying NO: poolYes increases
|
|
83
|
+
let new_pool_yes = pool_yes + shares * (pool_yes / pool_no);
|
|
84
|
+
new_pool_yes / (new_pool_yes + pool_no)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Calculate cost to buy shares (constant product AMM).
|
|
90
|
+
///
|
|
91
|
+
/// # Arguments
|
|
92
|
+
/// * `state` - Current AMM state
|
|
93
|
+
/// * `outcome` - Which outcome to buy
|
|
94
|
+
/// * `shares` - Number of shares to buy
|
|
95
|
+
///
|
|
96
|
+
/// # Returns
|
|
97
|
+
/// Cost in quote currency
|
|
98
|
+
///
|
|
99
|
+
/// # Example
|
|
100
|
+
/// ```
|
|
101
|
+
/// use quant_rust::prediction::amm::{AMMState, amm_buy_cost};
|
|
102
|
+
/// use quant_rust::prediction::Outcome;
|
|
103
|
+
/// let state = AMMState::new(1000.0, 1000.0, 1000.0, 0.0);
|
|
104
|
+
/// let cost = amm_buy_cost(&state, Outcome::Yes, 100.0);
|
|
105
|
+
/// assert!(cost > 0.0);
|
|
106
|
+
/// ```
|
|
107
|
+
pub fn amm_buy_cost(state: &AMMState, outcome: Outcome, shares: f64) -> f64 {
|
|
108
|
+
let AMMState { pool_yes, pool_no, k, .. } = *state;
|
|
109
|
+
|
|
110
|
+
match outcome {
|
|
111
|
+
Outcome::Yes => {
|
|
112
|
+
// Buying YES shares
|
|
113
|
+
// After: newPoolYes = poolYes + shares
|
|
114
|
+
// newPoolNo = k / (poolYes + shares)
|
|
115
|
+
let new_pool_no = k / (pool_yes + shares);
|
|
116
|
+
pool_no - new_pool_no
|
|
117
|
+
}
|
|
118
|
+
Outcome::No => {
|
|
119
|
+
// Buying NO shares
|
|
120
|
+
let new_pool_yes = k / (pool_no + shares);
|
|
121
|
+
pool_yes - new_pool_yes
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Calculate shares received for a given cost.
|
|
127
|
+
///
|
|
128
|
+
/// # Arguments
|
|
129
|
+
/// * `state` - Current AMM state
|
|
130
|
+
/// * `outcome` - Which outcome to buy
|
|
131
|
+
/// * `cost` - Amount to spend in quote currency
|
|
132
|
+
///
|
|
133
|
+
/// # Returns
|
|
134
|
+
/// Number of shares received
|
|
135
|
+
///
|
|
136
|
+
/// # Example
|
|
137
|
+
/// ```
|
|
138
|
+
/// use quant_rust::prediction::amm::{AMMState, amm_shares_received};
|
|
139
|
+
/// use quant_rust::prediction::Outcome;
|
|
140
|
+
/// let state = AMMState::new(1000.0, 1000.0, 1000.0, 0.0);
|
|
141
|
+
/// let shares = amm_shares_received(&state, Outcome::Yes, 50.0);
|
|
142
|
+
/// assert!(shares > 0.0);
|
|
143
|
+
/// ```
|
|
144
|
+
pub fn amm_shares_received(state: &AMMState, outcome: Outcome, cost: f64) -> f64 {
|
|
145
|
+
let AMMState { pool_yes, pool_no, k, .. } = *state;
|
|
146
|
+
|
|
147
|
+
match outcome {
|
|
148
|
+
Outcome::Yes => {
|
|
149
|
+
// Spending from NO pool side
|
|
150
|
+
let new_pool_no = pool_no - cost;
|
|
151
|
+
let new_pool_yes = k / new_pool_no;
|
|
152
|
+
new_pool_yes - pool_yes
|
|
153
|
+
}
|
|
154
|
+
Outcome::No => {
|
|
155
|
+
let new_pool_yes = pool_yes - cost;
|
|
156
|
+
let new_pool_no = k / new_pool_yes;
|
|
157
|
+
new_pool_no - pool_no
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Calculate price impact of a trade.
|
|
163
|
+
///
|
|
164
|
+
/// # Arguments
|
|
165
|
+
/// * `state` - Current AMM state
|
|
166
|
+
/// * `outcome` - Which outcome to buy
|
|
167
|
+
/// * `shares` - Number of shares to buy
|
|
168
|
+
///
|
|
169
|
+
/// # Returns
|
|
170
|
+
/// PriceImpactResult with detailed metrics
|
|
171
|
+
///
|
|
172
|
+
/// # Example
|
|
173
|
+
/// ```
|
|
174
|
+
/// use quant_rust::prediction::amm::{AMMState, amm_price_impact};
|
|
175
|
+
/// use quant_rust::prediction::Outcome;
|
|
176
|
+
/// let state = AMMState::new(1000.0, 1000.0, 1000.0, 0.0);
|
|
177
|
+
/// let impact = amm_price_impact(&state, Outcome::Yes, 100.0);
|
|
178
|
+
/// assert!(impact.price_impact > 0.0);
|
|
179
|
+
/// ```
|
|
180
|
+
pub fn amm_price_impact(state: &AMMState, outcome: Outcome, shares: f64) -> PriceImpactResult {
|
|
181
|
+
let AMMState { pool_yes, pool_no, .. } = *state;
|
|
182
|
+
|
|
183
|
+
// Current price
|
|
184
|
+
let current_price = match outcome {
|
|
185
|
+
Outcome::Yes => pool_no / (pool_yes + pool_no),
|
|
186
|
+
Outcome::No => pool_yes / (pool_yes + pool_no),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let new_price = amm_price_after_buy(state, outcome, shares);
|
|
190
|
+
let cost = amm_buy_cost(state, outcome, shares);
|
|
191
|
+
let avg_price = if shares > 0.0 { cost / shares } else { 0.0 };
|
|
192
|
+
|
|
193
|
+
let price_impact = if current_price > 0.0 {
|
|
194
|
+
(new_price - current_price).abs() / current_price
|
|
195
|
+
} else {
|
|
196
|
+
0.0
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let slippage = if current_price > 0.0 {
|
|
200
|
+
(avg_price - current_price).abs() / current_price
|
|
201
|
+
} else {
|
|
202
|
+
0.0
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
PriceImpactResult {
|
|
206
|
+
current_price,
|
|
207
|
+
new_price,
|
|
208
|
+
avg_price,
|
|
209
|
+
price_impact,
|
|
210
|
+
slippage,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Calculate impermanent loss for LP.
|
|
215
|
+
///
|
|
216
|
+
/// Impermanent loss is the loss incurred by providing liquidity compared to
|
|
217
|
+
/// simply holding the assets.
|
|
218
|
+
///
|
|
219
|
+
/// # Arguments
|
|
220
|
+
/// * `initial_price` - Initial price (as probability 0-1)
|
|
221
|
+
/// * `current_price` - Current price
|
|
222
|
+
///
|
|
223
|
+
/// # Returns
|
|
224
|
+
/// Impermanent loss as a decimal (e.g., 0.05 = 5% loss)
|
|
225
|
+
///
|
|
226
|
+
/// # Example
|
|
227
|
+
/// ```
|
|
228
|
+
/// use quant_rust::prediction::amm::amm_impermanent_loss;
|
|
229
|
+
/// let il = amm_impermanent_loss(0.5, 0.6);
|
|
230
|
+
/// assert!(il > 0.0);
|
|
231
|
+
/// ```
|
|
232
|
+
pub fn amm_impermanent_loss(initial_price: f64, current_price: f64) -> f64 {
|
|
233
|
+
if initial_price <= 0.0 || current_price <= 0.0 {
|
|
234
|
+
return 0.0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let price_ratio = current_price / initial_price;
|
|
238
|
+
|
|
239
|
+
// IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
|
|
240
|
+
let il = (2.0 * price_ratio.sqrt()) / (1.0 + price_ratio) - 1.0;
|
|
241
|
+
il.abs()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[cfg(test)]
|
|
245
|
+
mod tests {
|
|
246
|
+
use super::*;
|
|
247
|
+
|
|
248
|
+
fn create_balanced_pool() -> AMMState {
|
|
249
|
+
AMMState::new(1000.0, 1000.0, 1000.0, 0.0)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn test_amm_state_creation() {
|
|
254
|
+
let state = create_balanced_pool();
|
|
255
|
+
assert_eq!(state.pool_yes, 1000.0);
|
|
256
|
+
assert_eq!(state.pool_no, 1000.0);
|
|
257
|
+
assert_eq!(state.k, 1000000.0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn test_amm_buy_cost_yes() {
|
|
262
|
+
let state = create_balanced_pool();
|
|
263
|
+
let cost = amm_buy_cost(&state, Outcome::Yes, 100.0);
|
|
264
|
+
// Cost should be less than shares at current price (0.5)
|
|
265
|
+
// because we get a better rate initially
|
|
266
|
+
assert!(cost > 0.0);
|
|
267
|
+
assert!(cost < 100.0); // Less than 100 * 0.5 would be at current price
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn test_amm_buy_cost_no() {
|
|
272
|
+
let state = create_balanced_pool();
|
|
273
|
+
let cost = amm_buy_cost(&state, Outcome::No, 100.0);
|
|
274
|
+
assert!(cost > 0.0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn test_amm_price_after_buy() {
|
|
279
|
+
let state = create_balanced_pool();
|
|
280
|
+
let price_before = 0.5;
|
|
281
|
+
let price_after = amm_price_after_buy(&state, Outcome::Yes, 100.0);
|
|
282
|
+
// Buying YES should increase YES price
|
|
283
|
+
assert!(price_after > price_before);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn test_amm_shares_received() {
|
|
288
|
+
let state = create_balanced_pool();
|
|
289
|
+
let cost = 50.0;
|
|
290
|
+
let shares = amm_shares_received(&state, Outcome::Yes, cost);
|
|
291
|
+
assert!(shares > 0.0);
|
|
292
|
+
// Verify round-trip: shares received for cost should equal cost to buy shares
|
|
293
|
+
let cost_check = amm_buy_cost(&state, Outcome::Yes, shares);
|
|
294
|
+
assert!((cost - cost_check).abs() < 1e-6);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn test_amm_price_impact() {
|
|
299
|
+
let state = create_balanced_pool();
|
|
300
|
+
let impact = amm_price_impact(&state, Outcome::Yes, 100.0);
|
|
301
|
+
assert_eq!(impact.current_price, 0.5);
|
|
302
|
+
assert!(impact.new_price > 0.5);
|
|
303
|
+
assert!(impact.price_impact > 0.0);
|
|
304
|
+
assert!(impact.slippage >= 0.0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn test_amm_impermanent_loss_no_change() {
|
|
309
|
+
let il = amm_impermanent_loss(0.5, 0.5);
|
|
310
|
+
assert!((il - 0.0).abs() < 1e-10);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn test_amm_impermanent_loss_with_change() {
|
|
315
|
+
let il = amm_impermanent_loss(0.5, 0.6);
|
|
316
|
+
assert!(il > 0.0);
|
|
317
|
+
assert!(il < 0.1); // Should be relatively small for this price change
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn test_amm_impermanent_loss_symmetry() {
|
|
322
|
+
// IL is symmetric in terms of price RATIOS that are inverses
|
|
323
|
+
// price_ratio = 0.7/0.5 = 1.4 and price_ratio = 0.5/0.7 ≈ 0.714
|
|
324
|
+
// These give the same IL (using reciprocal ratios)
|
|
325
|
+
let il_up = amm_impermanent_loss(0.5, 0.7);
|
|
326
|
+
let il_down = amm_impermanent_loss(0.7, 0.5);
|
|
327
|
+
// IL should be the same for inverse price moves
|
|
328
|
+
assert!((il_up - il_down).abs() < 1e-10);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn test_large_trade_high_impact() {
|
|
333
|
+
let state = create_balanced_pool();
|
|
334
|
+
// Buying 50% of pool should cause significant impact
|
|
335
|
+
let impact = amm_price_impact(&state, Outcome::Yes, 500.0);
|
|
336
|
+
assert!(impact.price_impact > 0.1); // More than 10% impact
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
//! # Arbitrage Detection
|
|
2
|
+
//!
|
|
3
|
+
//! Detect arbitrage opportunities in prediction markets.
|
|
4
|
+
|
|
5
|
+
/// A single arbitrage opportunity
|
|
6
|
+
#[derive(Debug, Clone, Copy)]
|
|
7
|
+
pub struct ArbitrageOpportunity {
|
|
8
|
+
/// Price of YES shares
|
|
9
|
+
pub yes_price: f64,
|
|
10
|
+
/// Price of NO shares
|
|
11
|
+
pub no_price: f64,
|
|
12
|
+
/// Profit per $1 invested
|
|
13
|
+
pub profit: f64,
|
|
14
|
+
/// Amount to invest in YES
|
|
15
|
+
pub yes_size: f64,
|
|
16
|
+
/// Amount to invest in NO
|
|
17
|
+
pub no_size: f64,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Market price information for cross-market arbitrage
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
pub struct MarketPrice {
|
|
23
|
+
/// Market name/identifier
|
|
24
|
+
pub name: String,
|
|
25
|
+
/// YES price
|
|
26
|
+
pub yes_price: f64,
|
|
27
|
+
/// NO price
|
|
28
|
+
pub no_price: f64,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Cross-market arbitrage opportunity
|
|
32
|
+
#[derive(Debug, Clone)]
|
|
33
|
+
pub struct CrossMarketArbitrage {
|
|
34
|
+
/// Market to buy YES from
|
|
35
|
+
pub yes_market: String,
|
|
36
|
+
/// Market to buy NO from
|
|
37
|
+
pub no_market: String,
|
|
38
|
+
/// Price of YES
|
|
39
|
+
pub yes_price: f64,
|
|
40
|
+
/// Price of NO
|
|
41
|
+
pub no_price: f64,
|
|
42
|
+
/// Total cost (yes_price + no_price)
|
|
43
|
+
pub total_cost: f64,
|
|
44
|
+
/// Profit per $1 invested
|
|
45
|
+
pub profit: f64,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Detect arbitrage opportunity (YES + NO prices < 1).
|
|
49
|
+
///
|
|
50
|
+
/// If YES price + NO price < 1, you can buy both and guarantee a profit.
|
|
51
|
+
///
|
|
52
|
+
/// # Arguments
|
|
53
|
+
/// * `yes_price` - Price of YES shares (0-1)
|
|
54
|
+
/// * `no_price` - Price of NO shares (0-1)
|
|
55
|
+
///
|
|
56
|
+
/// # Returns
|
|
57
|
+
/// Some(ArbitrageOpportunity) if arbitrage exists, None otherwise
|
|
58
|
+
///
|
|
59
|
+
/// # Example
|
|
60
|
+
/// ```
|
|
61
|
+
/// use quant_rust::prediction::arbitrage::detect_arbitrage;
|
|
62
|
+
/// let arb = detect_arbitrage(0.45, 0.45);
|
|
63
|
+
/// assert!(arb.is_some());
|
|
64
|
+
/// assert!((arb.unwrap().profit - 0.10).abs() < 1e-10);
|
|
65
|
+
/// ```
|
|
66
|
+
pub fn detect_arbitrage(yes_price: f64, no_price: f64) -> Option<ArbitrageOpportunity> {
|
|
67
|
+
let total = yes_price + no_price;
|
|
68
|
+
|
|
69
|
+
if total >= 1.0 {
|
|
70
|
+
return None;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Buy equal amounts of both, guaranteed $1 back
|
|
74
|
+
let profit = 1.0 - total;
|
|
75
|
+
|
|
76
|
+
// For $1 investment: buy such that we get same payout either way
|
|
77
|
+
// If we spend $x on YES at price p_yes and $y on NO at price p_no
|
|
78
|
+
// x + y = 1 and x/p_yes = y/p_no (equal shares)
|
|
79
|
+
// x = p_yes / (p_yes + p_no), y = p_no / (p_yes + p_no)
|
|
80
|
+
let yes_size = yes_price / total;
|
|
81
|
+
let no_size = no_price / total;
|
|
82
|
+
|
|
83
|
+
Some(ArbitrageOpportunity {
|
|
84
|
+
yes_price,
|
|
85
|
+
no_price,
|
|
86
|
+
profit,
|
|
87
|
+
yes_size,
|
|
88
|
+
no_size,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Find best arbitrage across multiple markets on the same question.
|
|
93
|
+
///
|
|
94
|
+
/// Looks for cheapest YES and cheapest NO across all markets.
|
|
95
|
+
///
|
|
96
|
+
/// # Arguments
|
|
97
|
+
/// * `markets` - Slice of market prices
|
|
98
|
+
///
|
|
99
|
+
/// # Returns
|
|
100
|
+
/// Some(CrossMarketArbitrage) if arbitrage exists, None otherwise
|
|
101
|
+
///
|
|
102
|
+
/// # Example
|
|
103
|
+
/// ```
|
|
104
|
+
/// use quant_rust::prediction::arbitrage::{find_cross_market_arbitrage, MarketPrice};
|
|
105
|
+
/// let markets = vec![
|
|
106
|
+
/// MarketPrice { name: "market_a".to_string(), yes_price: 0.50, no_price: 0.55 },
|
|
107
|
+
/// MarketPrice { name: "market_b".to_string(), yes_price: 0.45, no_price: 0.60 },
|
|
108
|
+
/// ];
|
|
109
|
+
/// let arb = find_cross_market_arbitrage(&markets);
|
|
110
|
+
/// // Cheapest YES (0.45) + cheapest NO (0.55) = 1.0, no arb
|
|
111
|
+
/// assert!(arb.is_none());
|
|
112
|
+
/// ```
|
|
113
|
+
pub fn find_cross_market_arbitrage(markets: &[MarketPrice]) -> Option<CrossMarketArbitrage> {
|
|
114
|
+
if markets.is_empty() {
|
|
115
|
+
return None;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find cheapest YES and cheapest NO across markets
|
|
119
|
+
let mut cheapest_yes = Option::<(&MarketPrice, f64)>::None;
|
|
120
|
+
let mut cheapest_no = Option::<(&MarketPrice, f64)>::None;
|
|
121
|
+
|
|
122
|
+
for market in markets {
|
|
123
|
+
if cheapest_yes.is_none() || market.yes_price < cheapest_yes.unwrap().1 {
|
|
124
|
+
cheapest_yes = Some((market, market.yes_price));
|
|
125
|
+
}
|
|
126
|
+
if cheapest_no.is_none() || market.no_price < cheapest_no.unwrap().1 {
|
|
127
|
+
cheapest_no = Some((market, market.no_price));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let (yes_market, yes_price) = cheapest_yes?;
|
|
132
|
+
let (no_market, no_price) = cheapest_no?;
|
|
133
|
+
|
|
134
|
+
// Check if combined < 1
|
|
135
|
+
let total_cost = yes_price + no_price;
|
|
136
|
+
|
|
137
|
+
if total_cost >= 1.0 {
|
|
138
|
+
return None;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let profit = 1.0 - total_cost;
|
|
142
|
+
|
|
143
|
+
Some(CrossMarketArbitrage {
|
|
144
|
+
yes_market: yes_market.name.clone(),
|
|
145
|
+
no_market: no_market.name.clone(),
|
|
146
|
+
yes_price,
|
|
147
|
+
no_price,
|
|
148
|
+
total_cost,
|
|
149
|
+
profit,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[cfg(test)]
|
|
154
|
+
mod tests {
|
|
155
|
+
use super::*;
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_detect_arbitrage_exists() {
|
|
159
|
+
let arb = detect_arbitrage(0.45, 0.45);
|
|
160
|
+
assert!(arb.is_some());
|
|
161
|
+
let arb = arb.unwrap();
|
|
162
|
+
assert!((arb.profit - 0.10).abs() < 1e-10);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn test_detect_arbitrage_none() {
|
|
167
|
+
let arb = detect_arbitrage(0.55, 0.55);
|
|
168
|
+
assert!(arb.is_none());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_detect_arbitrage_exactly_one() {
|
|
173
|
+
let arb = detect_arbitrage(0.50, 0.50);
|
|
174
|
+
assert!(arb.is_none()); // No profit if exactly 1.0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn test_detect_arbitrage_sizes() {
|
|
179
|
+
let arb = detect_arbitrage(0.40, 0.50).unwrap();
|
|
180
|
+
// Total = 0.90, so yes_size = 0.40/0.90, no_size = 0.50/0.90
|
|
181
|
+
assert!((arb.yes_size - 0.4444).abs() < 0.01);
|
|
182
|
+
assert!((arb.no_size - 0.5556).abs() < 0.01);
|
|
183
|
+
assert!((arb.yes_size + arb.no_size - 1.0).abs() < 1e-10);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn test_find_cross_market_arbitrage_exists() {
|
|
188
|
+
let markets = vec![
|
|
189
|
+
MarketPrice { name: "a".to_string(), yes_price: 0.40, no_price: 0.65 },
|
|
190
|
+
MarketPrice { name: "b".to_string(), yes_price: 0.55, no_price: 0.45 },
|
|
191
|
+
];
|
|
192
|
+
let arb = find_cross_market_arbitrage(&markets);
|
|
193
|
+
assert!(arb.is_some());
|
|
194
|
+
let arb = arb.unwrap();
|
|
195
|
+
// Cheapest: YES=0.40 (a), NO=0.45 (b), total=0.85
|
|
196
|
+
assert_eq!(arb.yes_market, "a");
|
|
197
|
+
assert_eq!(arb.no_market, "b");
|
|
198
|
+
assert!((arb.profit - 0.15).abs() < 1e-10);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn test_find_cross_market_arbitrage_none() {
|
|
203
|
+
let markets = vec![
|
|
204
|
+
MarketPrice { name: "a".to_string(), yes_price: 0.55, no_price: 0.55 },
|
|
205
|
+
MarketPrice { name: "b".to_string(), yes_price: 0.60, no_price: 0.50 },
|
|
206
|
+
];
|
|
207
|
+
let arb = find_cross_market_arbitrage(&markets);
|
|
208
|
+
assert!(arb.is_none()); // Cheapest 0.55 + 0.50 = 1.05 > 1
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn test_find_cross_market_empty() {
|
|
213
|
+
let markets: Vec<MarketPrice> = vec![];
|
|
214
|
+
let arb = find_cross_market_arbitrage(&markets);
|
|
215
|
+
assert!(arb.is_none());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn test_find_cross_market_same_market() {
|
|
220
|
+
// Arb can exist within a single market
|
|
221
|
+
let markets = vec![
|
|
222
|
+
MarketPrice { name: "a".to_string(), yes_price: 0.45, no_price: 0.45 },
|
|
223
|
+
];
|
|
224
|
+
let arb = find_cross_market_arbitrage(&markets);
|
|
225
|
+
assert!(arb.is_some());
|
|
226
|
+
let arb = arb.unwrap();
|
|
227
|
+
assert_eq!(arb.yes_market, "a");
|
|
228
|
+
assert_eq!(arb.no_market, "a");
|
|
229
|
+
}
|
|
230
|
+
}
|