@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.
Files changed (60) hide show
  1. package/README.md +161 -0
  2. package/bun-ffi.d.ts +54 -0
  3. package/dist/index.js +576 -0
  4. package/dist/src/index.d.ts +324 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/types/index.d.ts +403 -0
  7. package/dist/types/index.d.ts.map +1 -0
  8. package/native/README.md +62 -0
  9. package/native/darwin-arm64/libquant_rust.dylib +0 -0
  10. package/package.json +70 -0
  11. package/scripts/postinstall.cjs +85 -0
  12. package/src/ffi.rs +496 -0
  13. package/src/index.ts +1073 -0
  14. package/src/indicators/ma.rs +222 -0
  15. package/src/indicators/mod.rs +18 -0
  16. package/src/indicators/momentum.rs +353 -0
  17. package/src/indicators/sr.rs +195 -0
  18. package/src/indicators/trend.rs +351 -0
  19. package/src/indicators/volatility.rs +270 -0
  20. package/src/indicators/volume.rs +213 -0
  21. package/src/lib.rs +130 -0
  22. package/src/patterns/breakout.rs +431 -0
  23. package/src/patterns/chart.rs +772 -0
  24. package/src/patterns/mod.rs +394 -0
  25. package/src/patterns/sr.rs +423 -0
  26. package/src/prediction/amm.rs +338 -0
  27. package/src/prediction/arbitrage.rs +230 -0
  28. package/src/prediction/calibration.rs +317 -0
  29. package/src/prediction/kelly.rs +232 -0
  30. package/src/prediction/lmsr.rs +194 -0
  31. package/src/prediction/mod.rs +59 -0
  32. package/src/prediction/odds.rs +229 -0
  33. package/src/prediction/pnl.rs +254 -0
  34. package/src/prediction/risk.rs +228 -0
  35. package/src/risk/beta.rs +257 -0
  36. package/src/risk/drawdown.rs +256 -0
  37. package/src/risk/leverage.rs +201 -0
  38. package/src/risk/mod.rs +388 -0
  39. package/src/risk/portfolio.rs +287 -0
  40. package/src/risk/ratios.rs +290 -0
  41. package/src/risk/sizing.rs +194 -0
  42. package/src/risk/var.rs +222 -0
  43. package/src/stats/cdf.rs +257 -0
  44. package/src/stats/correlation.rs +225 -0
  45. package/src/stats/distribution.rs +194 -0
  46. package/src/stats/hypothesis.rs +177 -0
  47. package/src/stats/matrix.rs +346 -0
  48. package/src/stats/mod.rs +257 -0
  49. package/src/stats/regression.rs +239 -0
  50. package/src/stats/rolling.rs +193 -0
  51. package/src/stats/timeseries.rs +263 -0
  52. package/src/types.rs +224 -0
  53. package/src/utils/mod.rs +215 -0
  54. package/src/utils/normalize.rs +192 -0
  55. package/src/utils/price.rs +167 -0
  56. package/src/utils/quantiles.rs +177 -0
  57. package/src/utils/returns.rs +158 -0
  58. package/src/utils/rolling.rs +97 -0
  59. package/src/utils/stats.rs +154 -0
  60. package/types/index.ts +513 -0
package/src/ffi.rs ADDED
@@ -0,0 +1,496 @@
1
+ //! FFI (Foreign Function Interface) exports for Bun
2
+ //!
3
+ //! This module provides C ABI compatible functions for calling from TypeScript/Bun.
4
+
5
+ use std::ffi::CString;
6
+ use std::os::raw::c_char;
7
+ use std::ptr;
8
+ use std::sync::Mutex;
9
+
10
+ use crate::types::*;
11
+
12
+ /// Thread-safe error storage
13
+ static LAST_ERROR: Mutex<Option<CString>> = Mutex::new(None);
14
+
15
+ /// Set the last error message
16
+ fn set_last_error(msg: &str) {
17
+ if let Ok(mut guard) = LAST_ERROR.lock() {
18
+ *guard = Some(CString::new(msg).unwrap_or_else(|_| CString::new("Invalid error message").unwrap()));
19
+ }
20
+ }
21
+
22
+ /// Get the last error message (returns null if no error)
23
+ #[no_mangle]
24
+ pub extern "C" fn quant_last_error() -> *const c_char {
25
+ if let Ok(guard) = LAST_ERROR.lock() {
26
+ if let Some(ref s) = *guard {
27
+ return s.as_ptr();
28
+ }
29
+ }
30
+ ptr::null()
31
+ }
32
+
33
+ /// Clear the last error
34
+ #[no_mangle]
35
+ pub extern "C" fn quant_clear_error() {
36
+ if let Ok(mut guard) = LAST_ERROR.lock() {
37
+ *guard = None;
38
+ }
39
+ }
40
+
41
+ /// Helper to return a JSON string via FFI
42
+ fn return_json<T: serde::Serialize>(result: Result<T, String>) -> *mut c_char {
43
+ match result {
44
+ Ok(value) => {
45
+ match serde_json::to_string(&value) {
46
+ Ok(json) => CString::new(json).unwrap().into_raw(),
47
+ Err(e) => {
48
+ set_last_error(&format!("JSON serialization error: {}", e));
49
+ ptr::null_mut()
50
+ }
51
+ }
52
+ }
53
+ Err(e) => {
54
+ set_last_error(&e);
55
+ ptr::null_mut()
56
+ }
57
+ }
58
+ }
59
+
60
+ /// Helper to return a JSON value directly
61
+ fn return_json_value(value: serde_json::Value) -> *mut c_char {
62
+ match serde_json::to_string(&value) {
63
+ Ok(json) => CString::new(json).unwrap().into_raw(),
64
+ Err(e) => {
65
+ set_last_error(&format!("JSON serialization error: {}", e));
66
+ ptr::null_mut()
67
+ }
68
+ }
69
+ }
70
+
71
+ // ============================================================================
72
+ // OHLCV Functions
73
+ // ============================================================================
74
+
75
+ /// Create an OHLCV struct and return as JSON
76
+ #[no_mangle]
77
+ pub extern "C" fn quant_ohlcv_new(
78
+ timestamp: i64,
79
+ open: f64,
80
+ high: f64,
81
+ low: f64,
82
+ close: f64,
83
+ volume: f64,
84
+ ) -> *mut c_char {
85
+ let ohlcv = OHLCV::new(timestamp, open, high, low, close, volume);
86
+ return_json(Ok(ohlcv))
87
+ }
88
+
89
+ /// Calculate typical price for OHLCV data array
90
+ /// Input: pointer to f64 array of [open, high, low, close, volume, open2, high2, ...]
91
+ /// count: number of OHLCV candles
92
+ /// Output: pointer to f64 array of typical prices (caller must free with quant_free_f64_array)
93
+ #[no_mangle]
94
+ pub extern "C" fn quant_ohlcv_typical_prices(
95
+ data: *const f64,
96
+ count: usize,
97
+ out_len: *mut usize,
98
+ ) -> *mut f64 {
99
+ if data.is_null() || out_len.is_null() {
100
+ set_last_error("Null pointer provided");
101
+ return ptr::null_mut();
102
+ }
103
+
104
+ let data_slice = unsafe { std::slice::from_raw_parts(data, count * 6) };
105
+ let mut results = Vec::with_capacity(count);
106
+
107
+ for i in 0..count {
108
+ let base = i * 6;
109
+ let high = data_slice[base + 1];
110
+ let low = data_slice[base + 2];
111
+ let close = data_slice[base + 3];
112
+ results.push((high + low + close) / 3.0);
113
+ }
114
+
115
+ unsafe { *out_len = results.len(); }
116
+ results.leak().as_mut_ptr()
117
+ }
118
+
119
+ // ============================================================================
120
+ // AMM Functions
121
+ // ============================================================================
122
+
123
+ /// Create an AMM state and return as JSON
124
+ #[no_mangle]
125
+ pub extern "C" fn quant_amm_new(pool_yes: f64, pool_no: f64, fee: f64) -> *mut c_char {
126
+ let amm = AMMState::new(pool_yes, pool_no, fee);
127
+ return_json(Ok(amm))
128
+ }
129
+
130
+ /// Calculate cost to buy shares from AMM
131
+ #[no_mangle]
132
+ pub extern "C" fn quant_amm_calculate_cost(
133
+ pool_yes: f64,
134
+ pool_no: f64,
135
+ buy_yes: bool,
136
+ shares: f64,
137
+ ) -> *mut c_char {
138
+ let result = if shares <= 0.0 {
139
+ Err("Shares must be positive".to_string())
140
+ } else if pool_yes <= 0.0 || pool_no <= 0.0 {
141
+ Err("Pool reserves must be positive".to_string())
142
+ } else {
143
+ let k = pool_yes * pool_no;
144
+ let cost = if buy_yes {
145
+ // Cost to buy YES shares: new_pool_no - pool_no where new_pool_no = k / (pool_yes + shares)
146
+ let new_pool_yes = pool_yes + shares;
147
+ let new_pool_no = k / new_pool_yes;
148
+ new_pool_no - pool_no
149
+ } else {
150
+ // Cost to buy NO shares
151
+ let new_pool_no = pool_no + shares;
152
+ let new_pool_yes = k / new_pool_no;
153
+ new_pool_yes - pool_yes
154
+ };
155
+ Ok(serde_json::json!({"cost": cost, "avg_price": cost / shares}))
156
+ };
157
+ match result {
158
+ Ok(value) => return_json_value(value),
159
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
160
+ }
161
+ }
162
+
163
+ /// Calculate price impact for a trade
164
+ #[no_mangle]
165
+ pub extern "C" fn quant_amm_price_impact(
166
+ pool_yes: f64,
167
+ pool_no: f64,
168
+ buy_yes: bool,
169
+ shares: f64,
170
+ ) -> *mut c_char {
171
+ let result = if shares <= 0.0 {
172
+ Err("Shares must be positive".to_string())
173
+ } else if pool_yes <= 0.0 || pool_no <= 0.0 {
174
+ Err("Pool reserves must be positive".to_string())
175
+ } else {
176
+ let initial_price = pool_no / (pool_yes + pool_no);
177
+ let k = pool_yes * pool_no;
178
+
179
+ let (new_pool_yes, new_pool_no) = if buy_yes {
180
+ let new_yes = pool_yes + shares;
181
+ (new_yes, k / new_yes)
182
+ } else {
183
+ let new_no = pool_no + shares;
184
+ (k / new_no, new_no)
185
+ };
186
+
187
+ let final_price = new_pool_no / (new_pool_yes + new_pool_no);
188
+ let impact = (final_price - initial_price).abs() / initial_price;
189
+
190
+ Ok(serde_json::json!({
191
+ "initial_price": initial_price,
192
+ "final_price": final_price,
193
+ "price_impact": impact,
194
+ "impact_bps": impact * 10000.0
195
+ }))
196
+ };
197
+ match result {
198
+ Ok(value) => return_json_value(value),
199
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
200
+ }
201
+ }
202
+
203
+ // ============================================================================
204
+ // LMSR Functions
205
+ // ============================================================================
206
+
207
+ /// Calculate LMSR price
208
+ #[no_mangle]
209
+ pub extern "C" fn quant_lmsr_price(yes_shares: f64, no_shares: f64, b: f64) -> *mut c_char {
210
+ let result = if b <= 0.0 {
211
+ Err("Liquidity parameter b must be positive".to_string())
212
+ } else {
213
+ let state = LMSRState::new(yes_shares, no_shares, b);
214
+ Ok(serde_json::json!({
215
+ "yes_price": state.yes_price(),
216
+ "no_price": state.no_price()
217
+ }))
218
+ };
219
+ match result {
220
+ Ok(value) => return_json_value(value),
221
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
222
+ }
223
+ }
224
+
225
+ /// Calculate LMSR cost to buy shares
226
+ #[no_mangle]
227
+ pub extern "C" fn quant_lmsr_cost(
228
+ yes_shares: f64,
229
+ no_shares: f64,
230
+ b: f64,
231
+ buy_yes: bool,
232
+ shares: f64,
233
+ ) -> *mut c_char {
234
+ let result = if b <= 0.0 {
235
+ Err("Liquidity parameter b must be positive".to_string())
236
+ } else if shares <= 0.0 {
237
+ Err("Shares must be positive".to_string())
238
+ } else {
239
+ // LMSR cost formula: C(q') - C(q) = b * ln(sum(exp(q'_i/b))) - b * ln(sum(exp(q_i/b)))
240
+ let cost = if buy_yes {
241
+ let before = b * ((yes_shares / b).exp() + (no_shares / b).exp()).ln();
242
+ let after = b * (((yes_shares + shares) / b).exp() + (no_shares / b).exp()).ln();
243
+ after - before
244
+ } else {
245
+ let before = b * ((yes_shares / b).exp() + (no_shares / b).exp()).ln();
246
+ let after = b * ((yes_shares / b).exp() + ((no_shares + shares) / b).exp()).ln();
247
+ after - before
248
+ };
249
+ Ok(serde_json::json!({"cost": cost, "avg_price": cost / shares}))
250
+ };
251
+ match result {
252
+ Ok(value) => return_json_value(value),
253
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
254
+ }
255
+ }
256
+
257
+ // ============================================================================
258
+ // Kelly Criterion Functions
259
+ // ============================================================================
260
+
261
+ /// Calculate Kelly criterion for prediction market
262
+ #[no_mangle]
263
+ pub extern "C" fn quant_kelly_criterion(
264
+ your_probability: f64,
265
+ market_price: f64,
266
+ bankroll: f64,
267
+ ) -> *mut c_char {
268
+ let result = if your_probability <= 0.0 || your_probability >= 1.0 {
269
+ Err("Probability must be between 0 and 1".to_string())
270
+ } else if market_price <= 0.0 || market_price >= 1.0 {
271
+ Err("Market price must be between 0 and 1".to_string())
272
+ } else if bankroll <= 0.0 {
273
+ Err("Bankroll must be positive".to_string())
274
+ } else {
275
+ let edge = your_probability - market_price;
276
+ let odds = (1.0 - market_price) / market_price;
277
+
278
+ // Kelly fraction: (bp - q) / b where b = odds, p = your_prob, q = 1 - your_prob
279
+ let kelly = (odds * your_probability - (1.0 - your_probability)) / odds;
280
+ let kelly = kelly.max(0.0); // Never bet negative
281
+
282
+ let half_kelly = kelly / 2.0;
283
+ let quarter_kelly = kelly / 4.0;
284
+
285
+ let result = KellyResult {
286
+ kelly_fraction: kelly,
287
+ half_kelly,
288
+ quarter_kelly,
289
+ full_bet_size: kelly * bankroll,
290
+ half_bet_size: half_kelly * bankroll,
291
+ quarter_bet_size: quarter_kelly * bankroll,
292
+ edge,
293
+ odds,
294
+ };
295
+ Ok(result)
296
+ };
297
+ return_json(result)
298
+ }
299
+
300
+ // ============================================================================
301
+ // Arbitrage Detection
302
+ // ============================================================================
303
+
304
+ /// Detect arbitrage opportunity (if yes_price + no_price < 1)
305
+ #[no_mangle]
306
+ pub extern "C" fn quant_detect_arbitrage(yes_price: f64, no_price: f64) -> *mut c_char {
307
+ let result = if yes_price < 0.0 || yes_price > 1.0 || no_price < 0.0 || no_price > 1.0 {
308
+ Err("Prices must be between 0 and 1".to_string())
309
+ } else {
310
+ let total = yes_price + no_price;
311
+ let has_arb = total < 1.0;
312
+ let profit_per_share = if has_arb { 1.0 - total } else { 0.0 };
313
+
314
+ Ok(serde_json::json!({
315
+ "has_arbitrage": has_arb,
316
+ "yes_price": yes_price,
317
+ "no_price": no_price,
318
+ "total_price": total,
319
+ "profit_per_share": profit_per_share,
320
+ "profit_bps": profit_per_share * 10000.0
321
+ }))
322
+ };
323
+ match result {
324
+ Ok(value) => return_json_value(value),
325
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
326
+ }
327
+ }
328
+
329
+ // ============================================================================
330
+ // Odds Conversion
331
+ // ============================================================================
332
+
333
+ /// Convert between probability, decimal odds, and American odds
334
+ #[no_mangle]
335
+ pub extern "C" fn quant_convert_odds(value: f64, from_type: i32) -> *mut c_char {
336
+ // from_type: 0 = probability, 1 = decimal odds, 2 = american odds
337
+ let result: Result<serde_json::Value, String> = match from_type {
338
+ 0 => {
339
+ // Input is probability
340
+ if value <= 0.0 || value >= 1.0 {
341
+ Err("Probability must be between 0 and 1".to_string())
342
+ } else {
343
+ let decimal = 1.0 / value;
344
+ let american = if value >= 0.5 {
345
+ -((value / (1.0 - value)) * 100.0).round()
346
+ } else {
347
+ ((1.0 - value) / value * 100.0).round()
348
+ };
349
+ Ok(serde_json::json!({
350
+ "probability": value,
351
+ "decimal_odds": decimal,
352
+ "american_odds": american
353
+ }))
354
+ }
355
+ }
356
+ 1 => {
357
+ // Input is decimal odds
358
+ if value < 1.0 {
359
+ Err("Decimal odds must be >= 1".to_string())
360
+ } else {
361
+ let probability = 1.0 / value;
362
+ let american = if probability >= 0.5 {
363
+ -((probability / (1.0 - probability)) * 100.0).round()
364
+ } else {
365
+ ((1.0 - probability) / probability * 100.0).round()
366
+ };
367
+ Ok(serde_json::json!({
368
+ "probability": probability,
369
+ "decimal_odds": value,
370
+ "american_odds": american
371
+ }))
372
+ }
373
+ }
374
+ 2 => {
375
+ // Input is American odds
376
+ if value == 0.0 {
377
+ Err("American odds cannot be 0".to_string())
378
+ } else {
379
+ let probability = if value > 0.0 {
380
+ 100.0 / (value + 100.0)
381
+ } else {
382
+ (-value) / ((-value) + 100.0)
383
+ };
384
+ let decimal = 1.0 / probability;
385
+ Ok(serde_json::json!({
386
+ "probability": probability,
387
+ "decimal_odds": decimal,
388
+ "american_odds": value
389
+ }))
390
+ }
391
+ }
392
+ _ => Err("Invalid from_type: must be 0 (probability), 1 (decimal), or 2 (american)".to_string()),
393
+ };
394
+ match result {
395
+ Ok(value) => return_json_value(value),
396
+ Err(e) => return_json(Err::<serde_json::Value, String>(e))
397
+ }
398
+ }
399
+
400
+ // ============================================================================
401
+ // Memory Management
402
+ // ============================================================================
403
+
404
+ /// Free a string returned from FFI
405
+ #[no_mangle]
406
+ pub extern "C" fn quant_free_string(s: *mut c_char) {
407
+ if !s.is_null() {
408
+ unsafe { drop(CString::from_raw(s)); }
409
+ }
410
+ }
411
+
412
+ /// Free a f64 array returned from FFI
413
+ #[no_mangle]
414
+ pub extern "C" fn quant_free_f64_array(ptr: *mut f64, len: usize) {
415
+ if !ptr.is_null() {
416
+ unsafe { drop(Vec::from_raw_parts(ptr, len, len)); }
417
+ }
418
+ }
419
+
420
+ /// Get library version
421
+ #[no_mangle]
422
+ pub extern "C" fn quant_version() -> *mut c_char {
423
+ CString::new(env!("CARGO_PKG_VERSION")).unwrap().into_raw()
424
+ }
425
+
426
+ // ============================================================================
427
+ // Simple Statistical Functions
428
+ // ============================================================================
429
+
430
+ /// Calculate mean of f64 array
431
+ #[no_mangle]
432
+ pub extern "C" fn quant_mean(data: *const f64, len: usize) -> f64 {
433
+ if data.is_null() || len == 0 {
434
+ return f64::NAN;
435
+ }
436
+ let slice = unsafe { std::slice::from_raw_parts(data, len) };
437
+ slice.iter().sum::<f64>() / len as f64
438
+ }
439
+
440
+ /// Calculate standard deviation of f64 array
441
+ #[no_mangle]
442
+ pub extern "C" fn quant_std_dev(data: *const f64, len: usize) -> f64 {
443
+ if data.is_null() || len == 0 {
444
+ return f64::NAN;
445
+ }
446
+ let slice = unsafe { std::slice::from_raw_parts(data, len) };
447
+ let mean = slice.iter().sum::<f64>() / len as f64;
448
+ let variance = slice.iter()
449
+ .map(|x| (x - mean).powi(2))
450
+ .sum::<f64>() / len as f64;
451
+ variance.sqrt()
452
+ }
453
+
454
+ /// Calculate variance of f64 array
455
+ #[no_mangle]
456
+ pub extern "C" fn quant_variance(data: *const f64, len: usize) -> f64 {
457
+ if data.is_null() || len == 0 {
458
+ return f64::NAN;
459
+ }
460
+ let slice = unsafe { std::slice::from_raw_parts(data, len) };
461
+ let mean = slice.iter().sum::<f64>() / len as f64;
462
+ slice.iter()
463
+ .map(|x| (x - mean).powi(2))
464
+ .sum::<f64>() / len as f64
465
+ }
466
+
467
+ /// Calculate correlation between two f64 arrays
468
+ #[no_mangle]
469
+ pub extern "C" fn quant_correlation(x: *const f64, y: *const f64, len: usize) -> f64 {
470
+ if x.is_null() || y.is_null() || len == 0 {
471
+ return f64::NAN;
472
+ }
473
+ let x_slice = unsafe { std::slice::from_raw_parts(x, len) };
474
+ let y_slice = unsafe { std::slice::from_raw_parts(y, len) };
475
+
476
+ let mean_x = x_slice.iter().sum::<f64>() / len as f64;
477
+ let mean_y = y_slice.iter().sum::<f64>() / len as f64;
478
+
479
+ let mut cov = 0.0;
480
+ let mut var_x = 0.0;
481
+ let mut var_y = 0.0;
482
+
483
+ for i in 0..len {
484
+ let dx = x_slice[i] - mean_x;
485
+ let dy = y_slice[i] - mean_y;
486
+ cov += dx * dy;
487
+ var_x += dx * dx;
488
+ var_y += dy * dy;
489
+ }
490
+
491
+ if var_x == 0.0 || var_y == 0.0 {
492
+ return f64::NAN;
493
+ }
494
+
495
+ cov / (var_x.sqrt() * var_y.sqrt())
496
+ }