nautilus_analysis/python/
analyzer.rs1use std::collections::{BTreeMap, HashMap};
17
18use nautilus_core::{UnixNanos, python::to_pyvalue_err};
19use nautilus_model::{
20 identifiers::PositionId,
21 position::Position,
22 types::{Currency, Money, fixed::check_float_precision},
23};
24use pyo3::prelude::*;
25
26use crate::{Returns, analyzer::PortfolioAnalyzer, python::statistic::statistic_from_pyobject};
27
28#[pymethods]
29#[pyo3_stub_gen::derive::gen_stub_pymethods]
30impl PortfolioAnalyzer {
31 #[new]
37 #[must_use]
38 pub fn py_new() -> Self {
39 Self::new()
40 }
41
42 fn __repr__(&self) -> String {
43 format!("PortfolioAnalyzer(currencies={})", self.currencies().len())
44 }
45
46 #[pyo3(name = "currencies")]
48 fn py_currencies(&self) -> Vec<Currency> {
49 self.currencies().into_iter().copied().collect()
50 }
51
52 #[pyo3(name = "get_performance_stats_returns")]
54 fn py_get_performance_stats_returns(&self) -> HashMap<String, f64> {
55 self.get_performance_stats_returns().into_iter().collect()
56 }
57
58 #[pyo3(name = "get_performance_stats_position_returns")]
60 fn py_get_performance_stats_position_returns(&self) -> HashMap<String, f64> {
61 self.get_performance_stats_position_returns()
62 .into_iter()
63 .collect()
64 }
65
66 #[pyo3(name = "get_performance_stats_portfolio_returns")]
68 fn py_get_performance_stats_portfolio_returns(&self) -> HashMap<String, f64> {
69 self.get_performance_stats_portfolio_returns()
70 .into_iter()
71 .collect()
72 }
73
74 #[pyo3(name = "get_performance_stats_returns_vs_benchmark")]
81 fn py_get_performance_stats_returns_vs_benchmark(
82 &self,
83 benchmark: BTreeMap<u64, f64>,
84 ) -> HashMap<String, f64> {
85 let benchmark: Returns = benchmark
86 .into_iter()
87 .map(|(k, v)| (UnixNanos::from(k), v))
88 .collect();
89 self.get_performance_stats_returns_vs_benchmark(&benchmark)
90 .into_iter()
91 .collect()
92 }
93
94 #[pyo3(name = "get_performance_stats_pnls")]
104 fn py_get_performance_stats_pnls(
105 &self,
106 currency: Option<&Currency>,
107 unrealized_pnl: Option<&Money>,
108 ) -> PyResult<HashMap<String, f64>> {
109 self.get_performance_stats_pnls(currency, unrealized_pnl)
110 .map(|m| m.into_iter().collect())
111 .map_err(to_pyvalue_err)
112 }
113
114 #[pyo3(name = "get_performance_stats_general")]
116 fn py_get_performance_stats_general(&self) -> HashMap<String, f64> {
117 self.get_performance_stats_general().into_iter().collect()
118 }
119
120 #[pyo3(name = "add_position_return")]
122 fn py_add_position_return(&mut self, timestamp: u64, value: f64) {
123 self.add_position_return(UnixNanos::from(timestamp), value);
124 }
125
126 #[pyo3(name = "add_return")]
130 fn py_add_return(&mut self, timestamp: u64, value: f64) {
131 self.add_return(UnixNanos::from(timestamp), value);
132 }
133
134 #[pyo3(name = "reset")]
138 fn py_reset(&mut self) {
139 self.reset();
140 }
141
142 #[pyo3(name = "register_statistic")]
144 fn py_register_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
145 self.register_statistic(statistic_from_pyobject(py, statistic)?);
146 Ok(())
147 }
148
149 #[pyo3(name = "deregister_statistic")]
151 fn py_deregister_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
152 self.deregister_statistic(&statistic_from_pyobject(py, statistic)?);
153 Ok(())
154 }
155
156 #[pyo3(name = "deregister_statistics")]
158 fn py_deregister_statistics(&mut self) {
159 self.deregister_statistics();
160 }
161
162 #[pyo3(name = "add_positions")]
164 #[expect(clippy::needless_pass_by_value)]
165 fn py_add_positions(&mut self, py: Python, positions: Vec<Py<PyAny>>) -> PyResult<()> {
166 let positions: Vec<Position> = positions
167 .iter()
168 .map(|position| position.extract::<Position>(py).map_err(Into::into))
169 .collect::<PyResult<Vec<Position>>>()?;
170
171 self.add_positions(&positions);
172 Ok(())
173 }
174
175 #[pyo3(name = "add_trade")]
177 #[allow(
178 clippy::trivially_copy_pass_by_ref,
179 reason = "matches underlying add_trade signature"
180 )]
181 fn py_add_trade(
182 &mut self,
183 position_id: &PositionId,
184 ts_event: u64,
185 realized_pnl: &Money,
186 ) -> PyResult<()> {
187 check_float_precision(realized_pnl.currency.precision).map_err(to_pyvalue_err)?;
188 self.add_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
189 Ok(())
190 }
191
192 #[pyo3(name = "record_trade")]
194 #[allow(
195 clippy::trivially_copy_pass_by_ref,
196 reason = "matches underlying record_trade signature"
197 )]
198 fn py_record_trade(
199 &mut self,
200 position_id: &PositionId,
201 ts_event: u64,
202 realized_pnl: &Money,
203 ) -> PyResult<()> {
204 check_float_precision(realized_pnl.currency.precision).map_err(to_pyvalue_err)?;
205 self.record_trade(position_id, UnixNanos::from(ts_event), realized_pnl);
206 Ok(())
207 }
208
209 #[pyo3(name = "statistic")]
214 fn py_statistic(&self, name: &str) -> Option<String> {
215 self.statistic(name).map(|s| s.name())
216 }
217
218 #[pyo3(name = "returns")]
223 fn py_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
224 let dict = pyo3::types::PyDict::new(py);
226 for (timestamp, value) in self.returns() {
227 dict.set_item(timestamp.as_u64(), value)?;
228 }
229 Ok(dict.into())
230 }
231
232 #[pyo3(name = "position_returns")]
234 fn py_position_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
235 let dict = pyo3::types::PyDict::new(py);
236 for (timestamp, value) in self.position_returns() {
237 dict.set_item(timestamp.as_u64(), value)?;
238 }
239 Ok(dict.into())
240 }
241
242 #[pyo3(name = "portfolio_returns")]
244 fn py_portfolio_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
245 let dict = pyo3::types::PyDict::new(py);
246 for (timestamp, value) in self.portfolio_returns() {
247 dict.set_item(timestamp.as_u64(), value)?;
248 }
249 Ok(dict.into())
250 }
251
252 #[pyo3(name = "realized_pnls")]
258 fn py_realized_pnls(&self, py: Python, currency: Option<&Currency>) -> PyResult<Py<PyAny>> {
259 match self.realized_pnls(currency) {
260 Some(pnls) => {
261 let list = pyo3::types::PyList::empty(py);
262 for (position_id, ts_event, pnl) in pnls {
263 list.append((position_id.to_string(), ts_event.as_u64(), pnl))?;
264 }
265 Ok(list.into())
266 }
267 None => Ok(py.None()),
268 }
269 }
270
271 #[pyo3(name = "total_pnl")]
280 fn py_total_pnl(
281 &self,
282 currency: Option<&Currency>,
283 unrealized_pnl: Option<&Money>,
284 ) -> PyResult<f64> {
285 self.total_pnl(currency, unrealized_pnl)
286 .map_err(to_pyvalue_err)
287 }
288
289 #[pyo3(name = "total_pnl_percentage")]
298 fn py_total_pnl_percentage(
299 &self,
300 currency: Option<&Currency>,
301 unrealized_pnl: Option<&Money>,
302 ) -> PyResult<f64> {
303 self.total_pnl_percentage(currency, unrealized_pnl)
304 .map_err(to_pyvalue_err)
305 }
306
307 #[pyo3(name = "get_stats_pnls_formatted")]
313 fn py_get_stats_pnls_formatted(
314 &self,
315 currency: Option<&Currency>,
316 unrealized_pnl: Option<&Money>,
317 ) -> PyResult<Vec<String>> {
318 self.get_stats_pnls_formatted(currency, unrealized_pnl)
319 .map_err(to_pyvalue_err)
320 }
321
322 #[pyo3(name = "get_stats_returns_formatted")]
324 fn py_get_stats_returns_formatted(&self) -> Vec<String> {
325 self.get_stats_returns_formatted()
326 }
327
328 #[pyo3(name = "get_stats_position_returns_formatted")]
330 fn py_get_stats_position_returns_formatted(&self) -> Vec<String> {
331 self.get_stats_position_returns_formatted()
332 }
333
334 #[pyo3(name = "get_stats_portfolio_returns_formatted")]
336 fn py_get_stats_portfolio_returns_formatted(&self) -> Vec<String> {
337 self.get_stats_portfolio_returns_formatted()
338 }
339
340 #[pyo3(name = "get_stats_general_formatted")]
342 fn py_get_stats_general_formatted(&self) -> Vec<String> {
343 self.get_stats_general_formatted()
344 }
345}