nautilus_analysis/python/
analyzer.rs1use std::{
17 collections::{BTreeMap, HashMap},
18 sync::Arc,
19};
20
21use nautilus_core::{UnixNanos, python::to_pyvalue_err};
22use nautilus_model::{
23 identifiers::PositionId,
24 position::Position,
25 types::{Currency, Money},
26};
27use pyo3::prelude::*;
28
29use crate::{
30 Returns,
31 analyzer::{PortfolioAnalyzer, Statistic},
32 statistics::{
33 alpha::Alpha, beta_ratio::BetaRatio, cagr::CAGR, calmar_ratio::CalmarRatio,
34 down_capture_ratio::DownCaptureRatio, expectancy::Expectancy,
35 expected_shortfall::ExpectedShortfall, information_ratio::InformationRatio,
36 long_ratio::LongRatio, loser_avg::AvgLoser, loser_max::MaxLoser, loser_min::MinLoser,
37 max_drawdown::MaxDrawdown, omega_ratio::OmegaRatio, profit_factor::ProfitFactor,
38 returns_avg::ReturnsAverage, returns_avg_loss::ReturnsAverageLoss,
39 returns_avg_win::ReturnsAverageWin, returns_kurtosis::ReturnsKurtosis,
40 returns_skewness::ReturnsSkewness, returns_volatility::ReturnsVolatility,
41 risk_return_ratio::RiskReturnRatio, sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio,
42 tail_ratio::TailRatio, tracking_error::TrackingError, treynor_ratio::TreynorRatio,
43 ulcer_index::UlcerIndex, up_capture_ratio::UpCaptureRatio, value_at_risk::ValueAtRisk,
44 win_rate::WinRate, winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
45 },
46};
47
48#[pymethods]
49#[pyo3_stub_gen::derive::gen_stub_pymethods]
50impl PortfolioAnalyzer {
51 #[new]
57 #[must_use]
58 pub fn py_new() -> Self {
59 Self::new()
60 }
61
62 fn __repr__(&self) -> String {
63 format!("PortfolioAnalyzer(currencies={})", self.currencies().len())
64 }
65
66 #[pyo3(name = "currencies")]
68 fn py_currencies(&self) -> Vec<Currency> {
69 self.currencies().into_iter().copied().collect()
70 }
71
72 #[pyo3(name = "get_performance_stats_returns")]
74 fn py_get_performance_stats_returns(&self) -> HashMap<String, f64> {
75 self.get_performance_stats_returns().into_iter().collect()
76 }
77
78 #[pyo3(name = "get_performance_stats_position_returns")]
80 fn py_get_performance_stats_position_returns(&self) -> HashMap<String, f64> {
81 self.get_performance_stats_position_returns()
82 .into_iter()
83 .collect()
84 }
85
86 #[pyo3(name = "get_performance_stats_portfolio_returns")]
88 fn py_get_performance_stats_portfolio_returns(&self) -> HashMap<String, f64> {
89 self.get_performance_stats_portfolio_returns()
90 .into_iter()
91 .collect()
92 }
93
94 #[pyo3(name = "get_performance_stats_returns_vs_benchmark")]
101 fn py_get_performance_stats_returns_vs_benchmark(
102 &self,
103 benchmark: BTreeMap<u64, f64>,
104 ) -> HashMap<String, f64> {
105 let benchmark: Returns = benchmark
106 .into_iter()
107 .map(|(k, v)| (UnixNanos::from(k), v))
108 .collect();
109 self.get_performance_stats_returns_vs_benchmark(&benchmark)
110 .into_iter()
111 .collect()
112 }
113
114 #[pyo3(name = "get_performance_stats_pnls")]
124 fn py_get_performance_stats_pnls(
125 &self,
126 currency: Option<&Currency>,
127 unrealized_pnl: Option<&Money>,
128 ) -> PyResult<HashMap<String, f64>> {
129 self.get_performance_stats_pnls(currency, unrealized_pnl)
130 .map(|m| m.into_iter().collect())
131 .map_err(to_pyvalue_err)
132 }
133
134 #[pyo3(name = "get_performance_stats_general")]
136 fn py_get_performance_stats_general(&self) -> HashMap<String, f64> {
137 self.get_performance_stats_general().into_iter().collect()
138 }
139
140 #[pyo3(name = "add_position_return")]
142 fn py_add_position_return(&mut self, timestamp: u64, value: f64) {
143 self.add_position_return(UnixNanos::from(timestamp), value);
144 }
145
146 #[pyo3(name = "add_return")]
150 fn py_add_return(&mut self, timestamp: u64, value: f64) {
151 self.add_return(UnixNanos::from(timestamp), value);
152 }
153
154 #[pyo3(name = "reset")]
156 fn py_reset(&mut self) {
157 self.reset();
158 }
159
160 #[pyo3(name = "register_statistic")]
162 #[expect(clippy::needless_pass_by_value)]
163 #[expect(clippy::too_many_lines)]
164 fn py_register_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
165 let type_name = statistic
166 .getattr(py, "__class__")?
167 .getattr(py, "__name__")?
168 .extract::<String>(py)?;
169
170 match type_name.as_str() {
171 "MaxWinner" => {
172 let stat = statistic.extract::<MaxWinner>(py)?;
173 self.register_statistic(Arc::new(stat));
174 }
175 "MinWinner" => {
176 let stat = statistic.extract::<MinWinner>(py)?;
177 self.register_statistic(Arc::new(stat));
178 }
179 "AvgWinner" => {
180 let stat = statistic.extract::<AvgWinner>(py)?;
181 self.register_statistic(Arc::new(stat));
182 }
183 "MaxLoser" => {
184 let stat = statistic.extract::<MaxLoser>(py)?;
185 self.register_statistic(Arc::new(stat));
186 }
187 "MinLoser" => {
188 let stat = statistic.extract::<MinLoser>(py)?;
189 self.register_statistic(Arc::new(stat));
190 }
191 "AvgLoser" => {
192 let stat = statistic.extract::<AvgLoser>(py)?;
193 self.register_statistic(Arc::new(stat));
194 }
195 "Expectancy" => {
196 let stat = statistic.extract::<Expectancy>(py)?;
197 self.register_statistic(Arc::new(stat));
198 }
199 "WinRate" => {
200 let stat = statistic.extract::<WinRate>(py)?;
201 self.register_statistic(Arc::new(stat));
202 }
203 "ReturnsVolatility" => {
204 let stat = statistic.extract::<ReturnsVolatility>(py)?;
205 self.register_statistic(Arc::new(stat));
206 }
207 "ReturnsAverage" => {
208 let stat = statistic.extract::<ReturnsAverage>(py)?;
209 self.register_statistic(Arc::new(stat));
210 }
211 "ReturnsAverageLoss" => {
212 let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
213 self.register_statistic(Arc::new(stat));
214 }
215 "ReturnsAverageWin" => {
216 let stat = statistic.extract::<ReturnsAverageWin>(py)?;
217 self.register_statistic(Arc::new(stat));
218 }
219 "SharpeRatio" => {
220 let stat = statistic.extract::<SharpeRatio>(py)?;
221 self.register_statistic(Arc::new(stat));
222 }
223 "SortinoRatio" => {
224 let stat = statistic.extract::<SortinoRatio>(py)?;
225 self.register_statistic(Arc::new(stat));
226 }
227 "ProfitFactor" => {
228 let stat = statistic.extract::<ProfitFactor>(py)?;
229 self.register_statistic(Arc::new(stat));
230 }
231 "RiskReturnRatio" => {
232 let stat = statistic.extract::<RiskReturnRatio>(py)?;
233 self.register_statistic(Arc::new(stat));
234 }
235 "LongRatio" => {
236 let stat = statistic.extract::<LongRatio>(py)?;
237 self.register_statistic(Arc::new(stat));
238 }
239 "CAGR" => {
240 let stat = statistic.extract::<CAGR>(py)?;
241 self.register_statistic(Arc::new(stat));
242 }
243 "CalmarRatio" => {
244 let stat = statistic.extract::<CalmarRatio>(py)?;
245 self.register_statistic(Arc::new(stat));
246 }
247 "MaxDrawdown" => {
248 let stat = statistic.extract::<MaxDrawdown>(py)?;
249 self.register_statistic(Arc::new(stat));
250 }
251 "Alpha" => {
252 let stat = statistic.extract::<Alpha>(py)?;
253 self.register_statistic(Arc::new(stat));
254 }
255 "BetaRatio" => {
256 let stat = statistic.extract::<BetaRatio>(py)?;
257 self.register_statistic(Arc::new(stat));
258 }
259 "DownCaptureRatio" => {
260 let stat = statistic.extract::<DownCaptureRatio>(py)?;
261 self.register_statistic(Arc::new(stat));
262 }
263 "InformationRatio" => {
264 let stat = statistic.extract::<InformationRatio>(py)?;
265 self.register_statistic(Arc::new(stat));
266 }
267 "TrackingError" => {
268 let stat = statistic.extract::<TrackingError>(py)?;
269 self.register_statistic(Arc::new(stat));
270 }
271 "TreynorRatio" => {
272 let stat = statistic.extract::<TreynorRatio>(py)?;
273 self.register_statistic(Arc::new(stat));
274 }
275 "ReturnsSkewness" => {
276 let stat = statistic.extract::<ReturnsSkewness>(py)?;
277 self.register_statistic(Arc::new(stat));
278 }
279 "ReturnsKurtosis" => {
280 let stat = statistic.extract::<ReturnsKurtosis>(py)?;
281 self.register_statistic(Arc::new(stat));
282 }
283 "TailRatio" => {
284 let stat = statistic.extract::<TailRatio>(py)?;
285 self.register_statistic(Arc::new(stat));
286 }
287 "UlcerIndex" => {
288 let stat = statistic.extract::<UlcerIndex>(py)?;
289 self.register_statistic(Arc::new(stat));
290 }
291 "OmegaRatio" => {
292 let stat = statistic.extract::<OmegaRatio>(py)?;
293 self.register_statistic(Arc::new(stat));
294 }
295 "ValueAtRisk" => {
296 let stat = statistic.extract::<ValueAtRisk>(py)?;
297 self.register_statistic(Arc::new(stat));
298 }
299 "ExpectedShortfall" => {
300 let stat = statistic.extract::<ExpectedShortfall>(py)?;
301 self.register_statistic(Arc::new(stat));
302 }
303 "UpCaptureRatio" => {
304 let stat = statistic.extract::<UpCaptureRatio>(py)?;
305 self.register_statistic(Arc::new(stat));
306 }
307 _ => {
308 return Err(to_pyvalue_err(format!(
309 "Unknown statistic type: {type_name}"
310 )));
311 }
312 }
313
314 Ok(())
315 }
316
317 #[pyo3(name = "deregister_statistic")]
319 #[expect(clippy::needless_pass_by_value)]
320 #[expect(clippy::too_many_lines)]
321 fn py_deregister_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
322 let type_name = statistic
323 .getattr(py, "__class__")?
324 .getattr(py, "__name__")?
325 .extract::<String>(py)?;
326
327 match type_name.as_str() {
328 "MaxWinner" => {
329 let stat = statistic.extract::<MaxWinner>(py)?;
330 self.deregister_statistic(&(Arc::new(stat) as Statistic));
331 }
332 "MinWinner" => {
333 let stat = statistic.extract::<MinWinner>(py)?;
334 self.deregister_statistic(&(Arc::new(stat) as Statistic));
335 }
336 "AvgWinner" => {
337 let stat = statistic.extract::<AvgWinner>(py)?;
338 self.deregister_statistic(&(Arc::new(stat) as Statistic));
339 }
340 "MaxLoser" => {
341 let stat = statistic.extract::<MaxLoser>(py)?;
342 self.deregister_statistic(&(Arc::new(stat) as Statistic));
343 }
344 "MinLoser" => {
345 let stat = statistic.extract::<MinLoser>(py)?;
346 self.deregister_statistic(&(Arc::new(stat) as Statistic));
347 }
348 "AvgLoser" => {
349 let stat = statistic.extract::<AvgLoser>(py)?;
350 self.deregister_statistic(&(Arc::new(stat) as Statistic));
351 }
352 "Expectancy" => {
353 let stat = statistic.extract::<Expectancy>(py)?;
354 self.deregister_statistic(&(Arc::new(stat) as Statistic));
355 }
356 "WinRate" => {
357 let stat = statistic.extract::<WinRate>(py)?;
358 self.deregister_statistic(&(Arc::new(stat) as Statistic));
359 }
360 "ReturnsVolatility" => {
361 let stat = statistic.extract::<ReturnsVolatility>(py)?;
362 self.deregister_statistic(&(Arc::new(stat) as Statistic));
363 }
364 "ReturnsAverage" => {
365 let stat = statistic.extract::<ReturnsAverage>(py)?;
366 self.deregister_statistic(&(Arc::new(stat) as Statistic));
367 }
368 "ReturnsAverageLoss" => {
369 let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
370 self.deregister_statistic(&(Arc::new(stat) as Statistic));
371 }
372 "ReturnsAverageWin" => {
373 let stat = statistic.extract::<ReturnsAverageWin>(py)?;
374 self.deregister_statistic(&(Arc::new(stat) as Statistic));
375 }
376 "SharpeRatio" => {
377 let stat = statistic.extract::<SharpeRatio>(py)?;
378 self.deregister_statistic(&(Arc::new(stat) as Statistic));
379 }
380 "SortinoRatio" => {
381 let stat = statistic.extract::<SortinoRatio>(py)?;
382 self.deregister_statistic(&(Arc::new(stat) as Statistic));
383 }
384 "ProfitFactor" => {
385 let stat = statistic.extract::<ProfitFactor>(py)?;
386 self.deregister_statistic(&(Arc::new(stat) as Statistic));
387 }
388 "RiskReturnRatio" => {
389 let stat = statistic.extract::<RiskReturnRatio>(py)?;
390 self.deregister_statistic(&(Arc::new(stat) as Statistic));
391 }
392 "LongRatio" => {
393 let stat = statistic.extract::<LongRatio>(py)?;
394 self.deregister_statistic(&(Arc::new(stat) as Statistic));
395 }
396 "CAGR" => {
397 let stat = statistic.extract::<CAGR>(py)?;
398 self.deregister_statistic(&(Arc::new(stat) as Statistic));
399 }
400 "CalmarRatio" => {
401 let stat = statistic.extract::<CalmarRatio>(py)?;
402 self.deregister_statistic(&(Arc::new(stat) as Statistic));
403 }
404 "MaxDrawdown" => {
405 let stat = statistic.extract::<MaxDrawdown>(py)?;
406 self.deregister_statistic(&(Arc::new(stat) as Statistic));
407 }
408 "Alpha" => {
409 let stat = statistic.extract::<Alpha>(py)?;
410 self.deregister_statistic(&(Arc::new(stat) as Statistic));
411 }
412 "BetaRatio" => {
413 let stat = statistic.extract::<BetaRatio>(py)?;
414 self.deregister_statistic(&(Arc::new(stat) as Statistic));
415 }
416 "DownCaptureRatio" => {
417 let stat = statistic.extract::<DownCaptureRatio>(py)?;
418 self.deregister_statistic(&(Arc::new(stat) as Statistic));
419 }
420 "InformationRatio" => {
421 let stat = statistic.extract::<InformationRatio>(py)?;
422 self.deregister_statistic(&(Arc::new(stat) as Statistic));
423 }
424 "TrackingError" => {
425 let stat = statistic.extract::<TrackingError>(py)?;
426 self.deregister_statistic(&(Arc::new(stat) as Statistic));
427 }
428 "TreynorRatio" => {
429 let stat = statistic.extract::<TreynorRatio>(py)?;
430 self.deregister_statistic(&(Arc::new(stat) as Statistic));
431 }
432 "ReturnsSkewness" => {
433 let stat = statistic.extract::<ReturnsSkewness>(py)?;
434 self.deregister_statistic(&(Arc::new(stat) as Statistic));
435 }
436 "ReturnsKurtosis" => {
437 let stat = statistic.extract::<ReturnsKurtosis>(py)?;
438 self.deregister_statistic(&(Arc::new(stat) as Statistic));
439 }
440 "TailRatio" => {
441 let stat = statistic.extract::<TailRatio>(py)?;
442 self.deregister_statistic(&(Arc::new(stat) as Statistic));
443 }
444 "UlcerIndex" => {
445 let stat = statistic.extract::<UlcerIndex>(py)?;
446 self.deregister_statistic(&(Arc::new(stat) as Statistic));
447 }
448 "OmegaRatio" => {
449 let stat = statistic.extract::<OmegaRatio>(py)?;
450 self.deregister_statistic(&(Arc::new(stat) as Statistic));
451 }
452 "ValueAtRisk" => {
453 let stat = statistic.extract::<ValueAtRisk>(py)?;
454 self.deregister_statistic(&(Arc::new(stat) as Statistic));
455 }
456 "ExpectedShortfall" => {
457 let stat = statistic.extract::<ExpectedShortfall>(py)?;
458 self.deregister_statistic(&(Arc::new(stat) as Statistic));
459 }
460 "UpCaptureRatio" => {
461 let stat = statistic.extract::<UpCaptureRatio>(py)?;
462 self.deregister_statistic(&(Arc::new(stat) as Statistic));
463 }
464 _ => {
465 return Err(to_pyvalue_err(format!(
466 "Unknown statistic type: {type_name}"
467 )));
468 }
469 }
470
471 Ok(())
472 }
473
474 #[pyo3(name = "deregister_statistics")]
476 fn py_deregister_statistics(&mut self) {
477 self.deregister_statistics();
478 }
479
480 #[pyo3(name = "add_positions")]
482 #[expect(clippy::needless_pass_by_value)]
483 fn py_add_positions(&mut self, py: Python, positions: Vec<Py<PyAny>>) -> PyResult<()> {
484 let positions: Vec<Position> = positions
485 .iter()
486 .map(|position| position.extract::<Position>(py).map_err(Into::into))
487 .collect::<PyResult<Vec<Position>>>()?;
488
489 self.add_positions(&positions);
490 Ok(())
491 }
492
493 #[pyo3(name = "add_trade")]
495 #[allow(
496 clippy::trivially_copy_pass_by_ref,
497 reason = "matches underlying add_trade signature"
498 )]
499 fn py_add_trade(&mut self, position_id: &PositionId, ts_event: u64, realized_pnl: &Money) {
500 self.add_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
501 }
502
503 #[pyo3(name = "record_trade")]
505 #[allow(
506 clippy::trivially_copy_pass_by_ref,
507 reason = "matches underlying record_trade signature"
508 )]
509 fn py_record_trade(&mut self, position_id: &PositionId, ts_event: u64, realized_pnl: &Money) {
510 self.record_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
511 }
512
513 #[pyo3(name = "statistic")]
518 fn py_statistic(&self, name: &str) -> Option<String> {
519 self.statistic(name).map(|s| s.name())
520 }
521
522 #[pyo3(name = "returns")]
527 fn py_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
528 let dict = pyo3::types::PyDict::new(py);
530 for (timestamp, value) in self.returns() {
531 dict.set_item(timestamp.as_u64(), value)?;
532 }
533 Ok(dict.into())
534 }
535
536 #[pyo3(name = "position_returns")]
538 fn py_position_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
539 let dict = pyo3::types::PyDict::new(py);
540 for (timestamp, value) in self.position_returns() {
541 dict.set_item(timestamp.as_u64(), value)?;
542 }
543 Ok(dict.into())
544 }
545
546 #[pyo3(name = "portfolio_returns")]
548 fn py_portfolio_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
549 let dict = pyo3::types::PyDict::new(py);
550 for (timestamp, value) in self.portfolio_returns() {
551 dict.set_item(timestamp.as_u64(), value)?;
552 }
553 Ok(dict.into())
554 }
555
556 #[pyo3(name = "realized_pnls")]
561 fn py_realized_pnls(&self, py: Python, currency: Option<&Currency>) -> PyResult<Py<PyAny>> {
562 match self.realized_pnls(currency) {
563 Some(pnls) => {
564 let list = pyo3::types::PyList::empty(py);
565 for (position_id, ts_event, pnl) in pnls {
566 list.append((position_id.to_string(), ts_event.as_u64(), pnl))?;
567 }
568 Ok(list.into())
569 }
570 None => Ok(py.None()),
571 }
572 }
573
574 #[pyo3(name = "total_pnl")]
583 fn py_total_pnl(
584 &self,
585 currency: Option<&Currency>,
586 unrealized_pnl: Option<&Money>,
587 ) -> PyResult<f64> {
588 self.total_pnl(currency, unrealized_pnl)
589 .map_err(to_pyvalue_err)
590 }
591
592 #[pyo3(name = "total_pnl_percentage")]
601 fn py_total_pnl_percentage(
602 &self,
603 currency: Option<&Currency>,
604 unrealized_pnl: Option<&Money>,
605 ) -> PyResult<f64> {
606 self.total_pnl_percentage(currency, unrealized_pnl)
607 .map_err(to_pyvalue_err)
608 }
609
610 #[pyo3(name = "get_stats_pnls_formatted")]
616 fn py_get_stats_pnls_formatted(
617 &self,
618 currency: Option<&Currency>,
619 unrealized_pnl: Option<&Money>,
620 ) -> PyResult<Vec<String>> {
621 self.get_stats_pnls_formatted(currency, unrealized_pnl)
622 .map_err(to_pyvalue_err)
623 }
624
625 #[pyo3(name = "get_stats_returns_formatted")]
627 fn py_get_stats_returns_formatted(&self) -> Vec<String> {
628 self.get_stats_returns_formatted()
629 }
630
631 #[pyo3(name = "get_stats_position_returns_formatted")]
633 fn py_get_stats_position_returns_formatted(&self) -> Vec<String> {
634 self.get_stats_position_returns_formatted()
635 }
636
637 #[pyo3(name = "get_stats_portfolio_returns_formatted")]
639 fn py_get_stats_portfolio_returns_formatted(&self) -> Vec<String> {
640 self.get_stats_portfolio_returns_formatted()
641 }
642
643 #[pyo3(name = "get_stats_general_formatted")]
645 fn py_get_stats_general_formatted(&self) -> Vec<String> {
646 self.get_stats_general_formatted()
647 }
648}