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, expectancy::Expectancy,
34 information_ratio::InformationRatio, long_ratio::LongRatio, loser_avg::AvgLoser,
35 loser_max::MaxLoser, loser_min::MinLoser, profit_factor::ProfitFactor,
36 returns_avg::ReturnsAverage, returns_avg_loss::ReturnsAverageLoss,
37 returns_avg_win::ReturnsAverageWin, returns_volatility::ReturnsVolatility,
38 risk_return_ratio::RiskReturnRatio, sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio,
39 tracking_error::TrackingError, treynor_ratio::TreynorRatio, win_rate::WinRate,
40 winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
41 },
42};
43
44#[pymethods]
45#[pyo3_stub_gen::derive::gen_stub_pymethods]
46impl PortfolioAnalyzer {
47 #[new]
53 #[must_use]
54 pub fn py_new() -> Self {
55 Self::new()
56 }
57
58 fn __repr__(&self) -> String {
59 format!("PortfolioAnalyzer(currencies={})", self.currencies().len())
60 }
61
62 #[pyo3(name = "currencies")]
64 fn py_currencies(&self) -> Vec<Currency> {
65 self.currencies().into_iter().copied().collect()
66 }
67
68 #[pyo3(name = "get_performance_stats_returns")]
70 fn py_get_performance_stats_returns(&self) -> HashMap<String, f64> {
71 self.get_performance_stats_returns().into_iter().collect()
72 }
73
74 #[pyo3(name = "get_performance_stats_position_returns")]
76 fn py_get_performance_stats_position_returns(&self) -> HashMap<String, f64> {
77 self.get_performance_stats_position_returns()
78 .into_iter()
79 .collect()
80 }
81
82 #[pyo3(name = "get_performance_stats_portfolio_returns")]
84 fn py_get_performance_stats_portfolio_returns(&self) -> HashMap<String, f64> {
85 self.get_performance_stats_portfolio_returns()
86 .into_iter()
87 .collect()
88 }
89
90 #[pyo3(name = "get_performance_stats_returns_vs_benchmark")]
97 fn py_get_performance_stats_returns_vs_benchmark(
98 &self,
99 benchmark: BTreeMap<u64, f64>,
100 ) -> HashMap<String, f64> {
101 let benchmark: Returns = benchmark
102 .into_iter()
103 .map(|(k, v)| (UnixNanos::from(k), v))
104 .collect();
105 self.get_performance_stats_returns_vs_benchmark(&benchmark)
106 .into_iter()
107 .collect()
108 }
109
110 #[pyo3(name = "get_performance_stats_pnls")]
111 fn py_get_performance_stats_pnls(
112 &self,
113 currency: Option<&Currency>,
114 unrealized_pnl: Option<&Money>,
115 ) -> PyResult<HashMap<String, f64>> {
116 self.get_performance_stats_pnls(currency, unrealized_pnl)
117 .map(|m| m.into_iter().collect())
118 .map_err(to_pyvalue_err)
119 }
120
121 #[pyo3(name = "get_performance_stats_general")]
123 fn py_get_performance_stats_general(&self) -> HashMap<String, f64> {
124 self.get_performance_stats_general().into_iter().collect()
125 }
126
127 #[pyo3(name = "add_position_return")]
129 fn py_add_position_return(&mut self, timestamp: u64, value: f64) {
130 self.add_position_return(UnixNanos::from(timestamp), value);
131 }
132
133 #[pyo3(name = "add_return")]
137 fn py_add_return(&mut self, timestamp: u64, value: f64) {
138 self.add_return(UnixNanos::from(timestamp), value);
139 }
140
141 #[pyo3(name = "reset")]
143 fn py_reset(&mut self) {
144 self.reset();
145 }
146
147 #[pyo3(name = "register_statistic")]
149 #[expect(clippy::needless_pass_by_value)]
150 fn py_register_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
151 let type_name = statistic
152 .getattr(py, "__class__")?
153 .getattr(py, "__name__")?
154 .extract::<String>(py)?;
155
156 match type_name.as_str() {
157 "MaxWinner" => {
158 let stat = statistic.extract::<MaxWinner>(py)?;
159 self.register_statistic(Arc::new(stat));
160 }
161 "MinWinner" => {
162 let stat = statistic.extract::<MinWinner>(py)?;
163 self.register_statistic(Arc::new(stat));
164 }
165 "AvgWinner" => {
166 let stat = statistic.extract::<AvgWinner>(py)?;
167 self.register_statistic(Arc::new(stat));
168 }
169 "MaxLoser" => {
170 let stat = statistic.extract::<MaxLoser>(py)?;
171 self.register_statistic(Arc::new(stat));
172 }
173 "MinLoser" => {
174 let stat = statistic.extract::<MinLoser>(py)?;
175 self.register_statistic(Arc::new(stat));
176 }
177 "AvgLoser" => {
178 let stat = statistic.extract::<AvgLoser>(py)?;
179 self.register_statistic(Arc::new(stat));
180 }
181 "Expectancy" => {
182 let stat = statistic.extract::<Expectancy>(py)?;
183 self.register_statistic(Arc::new(stat));
184 }
185 "WinRate" => {
186 let stat = statistic.extract::<WinRate>(py)?;
187 self.register_statistic(Arc::new(stat));
188 }
189 "ReturnsVolatility" => {
190 let stat = statistic.extract::<ReturnsVolatility>(py)?;
191 self.register_statistic(Arc::new(stat));
192 }
193 "ReturnsAverage" => {
194 let stat = statistic.extract::<ReturnsAverage>(py)?;
195 self.register_statistic(Arc::new(stat));
196 }
197 "ReturnsAverageLoss" => {
198 let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
199 self.register_statistic(Arc::new(stat));
200 }
201 "ReturnsAverageWin" => {
202 let stat = statistic.extract::<ReturnsAverageWin>(py)?;
203 self.register_statistic(Arc::new(stat));
204 }
205 "SharpeRatio" => {
206 let stat = statistic.extract::<SharpeRatio>(py)?;
207 self.register_statistic(Arc::new(stat));
208 }
209 "SortinoRatio" => {
210 let stat = statistic.extract::<SortinoRatio>(py)?;
211 self.register_statistic(Arc::new(stat));
212 }
213 "ProfitFactor" => {
214 let stat = statistic.extract::<ProfitFactor>(py)?;
215 self.register_statistic(Arc::new(stat));
216 }
217 "RiskReturnRatio" => {
218 let stat = statistic.extract::<RiskReturnRatio>(py)?;
219 self.register_statistic(Arc::new(stat));
220 }
221 "LongRatio" => {
222 let stat = statistic.extract::<LongRatio>(py)?;
223 self.register_statistic(Arc::new(stat));
224 }
225 "Alpha" => {
226 let stat = statistic.extract::<Alpha>(py)?;
227 self.register_statistic(Arc::new(stat));
228 }
229 "BetaRatio" => {
230 let stat = statistic.extract::<BetaRatio>(py)?;
231 self.register_statistic(Arc::new(stat));
232 }
233 "InformationRatio" => {
234 let stat = statistic.extract::<InformationRatio>(py)?;
235 self.register_statistic(Arc::new(stat));
236 }
237 "TrackingError" => {
238 let stat = statistic.extract::<TrackingError>(py)?;
239 self.register_statistic(Arc::new(stat));
240 }
241 "TreynorRatio" => {
242 let stat = statistic.extract::<TreynorRatio>(py)?;
243 self.register_statistic(Arc::new(stat));
244 }
245 _ => {
246 return Err(to_pyvalue_err(format!(
247 "Unknown statistic type: {type_name}"
248 )));
249 }
250 }
251
252 Ok(())
253 }
254
255 #[pyo3(name = "deregister_statistic")]
257 #[expect(clippy::needless_pass_by_value)]
258 fn py_deregister_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
259 let type_name = statistic
260 .getattr(py, "__class__")?
261 .getattr(py, "__name__")?
262 .extract::<String>(py)?;
263
264 match type_name.as_str() {
265 "MaxWinner" => {
266 let stat = statistic.extract::<MaxWinner>(py)?;
267 self.deregister_statistic(&(Arc::new(stat) as Statistic));
268 }
269 "MinWinner" => {
270 let stat = statistic.extract::<MinWinner>(py)?;
271 self.deregister_statistic(&(Arc::new(stat) as Statistic));
272 }
273 "AvgWinner" => {
274 let stat = statistic.extract::<AvgWinner>(py)?;
275 self.deregister_statistic(&(Arc::new(stat) as Statistic));
276 }
277 "MaxLoser" => {
278 let stat = statistic.extract::<MaxLoser>(py)?;
279 self.deregister_statistic(&(Arc::new(stat) as Statistic));
280 }
281 "MinLoser" => {
282 let stat = statistic.extract::<MinLoser>(py)?;
283 self.deregister_statistic(&(Arc::new(stat) as Statistic));
284 }
285 "AvgLoser" => {
286 let stat = statistic.extract::<AvgLoser>(py)?;
287 self.deregister_statistic(&(Arc::new(stat) as Statistic));
288 }
289 "Expectancy" => {
290 let stat = statistic.extract::<Expectancy>(py)?;
291 self.deregister_statistic(&(Arc::new(stat) as Statistic));
292 }
293 "WinRate" => {
294 let stat = statistic.extract::<WinRate>(py)?;
295 self.deregister_statistic(&(Arc::new(stat) as Statistic));
296 }
297 "ReturnsVolatility" => {
298 let stat = statistic.extract::<ReturnsVolatility>(py)?;
299 self.deregister_statistic(&(Arc::new(stat) as Statistic));
300 }
301 "ReturnsAverage" => {
302 let stat = statistic.extract::<ReturnsAverage>(py)?;
303 self.deregister_statistic(&(Arc::new(stat) as Statistic));
304 }
305 "ReturnsAverageLoss" => {
306 let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
307 self.deregister_statistic(&(Arc::new(stat) as Statistic));
308 }
309 "ReturnsAverageWin" => {
310 let stat = statistic.extract::<ReturnsAverageWin>(py)?;
311 self.deregister_statistic(&(Arc::new(stat) as Statistic));
312 }
313 "SharpeRatio" => {
314 let stat = statistic.extract::<SharpeRatio>(py)?;
315 self.deregister_statistic(&(Arc::new(stat) as Statistic));
316 }
317 "SortinoRatio" => {
318 let stat = statistic.extract::<SortinoRatio>(py)?;
319 self.deregister_statistic(&(Arc::new(stat) as Statistic));
320 }
321 "ProfitFactor" => {
322 let stat = statistic.extract::<ProfitFactor>(py)?;
323 self.deregister_statistic(&(Arc::new(stat) as Statistic));
324 }
325 "RiskReturnRatio" => {
326 let stat = statistic.extract::<RiskReturnRatio>(py)?;
327 self.deregister_statistic(&(Arc::new(stat) as Statistic));
328 }
329 "LongRatio" => {
330 let stat = statistic.extract::<LongRatio>(py)?;
331 self.deregister_statistic(&(Arc::new(stat) as Statistic));
332 }
333 "Alpha" => {
334 let stat = statistic.extract::<Alpha>(py)?;
335 self.deregister_statistic(&(Arc::new(stat) as Statistic));
336 }
337 "BetaRatio" => {
338 let stat = statistic.extract::<BetaRatio>(py)?;
339 self.deregister_statistic(&(Arc::new(stat) as Statistic));
340 }
341 "InformationRatio" => {
342 let stat = statistic.extract::<InformationRatio>(py)?;
343 self.deregister_statistic(&(Arc::new(stat) as Statistic));
344 }
345 "TrackingError" => {
346 let stat = statistic.extract::<TrackingError>(py)?;
347 self.deregister_statistic(&(Arc::new(stat) as Statistic));
348 }
349 "TreynorRatio" => {
350 let stat = statistic.extract::<TreynorRatio>(py)?;
351 self.deregister_statistic(&(Arc::new(stat) as Statistic));
352 }
353 _ => {
354 return Err(to_pyvalue_err(format!(
355 "Unknown statistic type: {type_name}"
356 )));
357 }
358 }
359
360 Ok(())
361 }
362
363 #[pyo3(name = "deregister_statistics")]
365 fn py_deregister_statistics(&mut self) {
366 self.deregister_statistics();
367 }
368
369 #[pyo3(name = "add_positions")]
371 #[expect(clippy::needless_pass_by_value)]
372 fn py_add_positions(&mut self, py: Python, positions: Vec<Py<PyAny>>) -> PyResult<()> {
373 let positions: Vec<Position> = positions
375 .iter()
376 .map(|p| {
377 p.getattr(py, "_mem")?
380 .extract::<Position>(py)
381 .map_err(Into::into)
382 })
383 .collect::<PyResult<Vec<Position>>>()?;
384
385 self.add_positions(&positions);
386 Ok(())
387 }
388
389 #[pyo3(name = "add_trade")]
391 #[allow(
392 clippy::trivially_copy_pass_by_ref,
393 reason = "matches underlying add_trade signature"
394 )]
395 fn py_add_trade(&mut self, position_id: &PositionId, ts_event: u64, realized_pnl: &Money) {
396 self.add_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
397 }
398
399 #[pyo3(name = "record_trade")]
401 #[allow(
402 clippy::trivially_copy_pass_by_ref,
403 reason = "matches underlying record_trade signature"
404 )]
405 fn py_record_trade(&mut self, position_id: &PositionId, ts_event: u64, realized_pnl: &Money) {
406 self.record_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
407 }
408
409 #[pyo3(name = "statistic")]
414 fn py_statistic(&self, name: &str) -> Option<String> {
415 self.statistic(name).map(|s| s.name())
416 }
417
418 #[pyo3(name = "returns")]
423 fn py_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
424 let dict = pyo3::types::PyDict::new(py);
426 for (timestamp, value) in self.returns() {
427 dict.set_item(timestamp.as_u64(), value)?;
428 }
429 Ok(dict.into())
430 }
431
432 #[pyo3(name = "position_returns")]
434 fn py_position_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
435 let dict = pyo3::types::PyDict::new(py);
436 for (timestamp, value) in self.position_returns() {
437 dict.set_item(timestamp.as_u64(), value)?;
438 }
439 Ok(dict.into())
440 }
441
442 #[pyo3(name = "portfolio_returns")]
444 fn py_portfolio_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
445 let dict = pyo3::types::PyDict::new(py);
446 for (timestamp, value) in self.portfolio_returns() {
447 dict.set_item(timestamp.as_u64(), value)?;
448 }
449 Ok(dict.into())
450 }
451
452 #[pyo3(name = "realized_pnls")]
457 fn py_realized_pnls(&self, py: Python, currency: Option<&Currency>) -> PyResult<Py<PyAny>> {
458 match self.realized_pnls(currency) {
459 Some(pnls) => {
460 let list = pyo3::types::PyList::empty(py);
461 for (position_id, ts_event, pnl) in pnls {
462 list.append((position_id.to_string(), ts_event.as_u64(), pnl))?;
463 }
464 Ok(list.into())
465 }
466 None => Ok(py.None()),
467 }
468 }
469
470 #[pyo3(name = "total_pnl")]
471 fn py_total_pnl(
472 &self,
473 currency: Option<&Currency>,
474 unrealized_pnl: Option<&Money>,
475 ) -> PyResult<f64> {
476 self.total_pnl(currency, unrealized_pnl)
477 .map_err(to_pyvalue_err)
478 }
479
480 #[pyo3(name = "total_pnl_percentage")]
481 fn py_total_pnl_percentage(
482 &self,
483 currency: Option<&Currency>,
484 unrealized_pnl: Option<&Money>,
485 ) -> PyResult<f64> {
486 self.total_pnl_percentage(currency, unrealized_pnl)
487 .map_err(to_pyvalue_err)
488 }
489
490 #[pyo3(name = "get_stats_pnls_formatted")]
492 fn py_get_stats_pnls_formatted(
493 &self,
494 currency: Option<&Currency>,
495 unrealized_pnl: Option<&Money>,
496 ) -> PyResult<Vec<String>> {
497 self.get_stats_pnls_formatted(currency, unrealized_pnl)
498 .map_err(to_pyvalue_err)
499 }
500
501 #[pyo3(name = "get_stats_returns_formatted")]
503 fn py_get_stats_returns_formatted(&self) -> Vec<String> {
504 self.get_stats_returns_formatted()
505 }
506
507 #[pyo3(name = "get_stats_position_returns_formatted")]
509 fn py_get_stats_position_returns_formatted(&self) -> Vec<String> {
510 self.get_stats_position_returns_formatted()
511 }
512
513 #[pyo3(name = "get_stats_portfolio_returns_formatted")]
515 fn py_get_stats_portfolio_returns_formatted(&self) -> Vec<String> {
516 self.get_stats_portfolio_returns_formatted()
517 }
518
519 #[pyo3(name = "get_stats_general_formatted")]
521 fn py_get_stats_general_formatted(&self) -> Vec<String> {
522 self.get_stats_general_formatted()
523 }
524}