nautilus_analysis/python/
analyzer.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{collections::HashMap, sync::Arc};
17
18use nautilus_core::{UnixNanos, python::to_pyvalue_err};
19use nautilus_model::{
20    identifiers::PositionId,
21    position::Position,
22    types::{Currency, Money},
23};
24use pyo3::{exceptions::PyValueError, prelude::*};
25
26use crate::{
27    analyzer::PortfolioAnalyzer,
28    statistics::{
29        expectancy::Expectancy, long_ratio::LongRatio, loser_avg::AvgLoser, loser_max::MaxLoser,
30        loser_min::MinLoser, profit_factor::ProfitFactor, returns_avg::ReturnsAverage,
31        returns_avg_loss::ReturnsAverageLoss, returns_avg_win::ReturnsAverageWin,
32        returns_volatility::ReturnsVolatility, risk_return_ratio::RiskReturnRatio,
33        sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio, win_rate::WinRate,
34        winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
35    },
36};
37
38#[pymethods]
39impl PortfolioAnalyzer {
40    #[new]
41    #[must_use]
42    pub fn py_new() -> Self {
43        Self::new()
44    }
45
46    fn __repr__(&self) -> String {
47        format!("PortfolioAnalyzer(currencies={})", self.currencies().len())
48    }
49
50    #[pyo3(name = "currencies")]
51    fn py_currencies(&self) -> Vec<Currency> {
52        self.currencies().into_iter().copied().collect()
53    }
54
55    #[pyo3(name = "get_performance_stats_returns")]
56    fn py_get_performance_stats_returns(&self) -> HashMap<String, f64> {
57        self.get_performance_stats_returns()
58    }
59
60    #[pyo3(name = "get_performance_stats_pnls")]
61    fn py_get_performance_stats_pnls(
62        &self,
63        currency: Option<&Currency>,
64        unrealized_pnl: Option<&Money>,
65    ) -> PyResult<HashMap<String, f64>> {
66        self.get_performance_stats_pnls(currency, unrealized_pnl)
67            .map_err(to_pyvalue_err)
68    }
69
70    #[pyo3(name = "get_performance_stats_general")]
71    fn py_get_performance_stats_general(&self) -> HashMap<String, f64> {
72        self.get_performance_stats_general()
73    }
74
75    #[pyo3(name = "add_return")]
76    fn py_add_return(&mut self, timestamp: u64, value: f64) {
77        self.add_return(UnixNanos::from(timestamp), value);
78    }
79
80    #[pyo3(name = "reset")]
81    fn py_reset(&mut self) {
82        self.reset();
83    }
84
85    #[pyo3(name = "register_statistic")]
86    fn py_register_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
87        let type_name = statistic
88            .getattr(py, "__class__")?
89            .getattr(py, "__name__")?
90            .extract::<String>(py)?;
91
92        match type_name.as_str() {
93            "MaxWinner" => {
94                let stat = statistic.extract::<MaxWinner>(py)?;
95                self.register_statistic(Arc::new(stat));
96            }
97            "MinWinner" => {
98                let stat = statistic.extract::<MinWinner>(py)?;
99                self.register_statistic(Arc::new(stat));
100            }
101            "AvgWinner" => {
102                let stat = statistic.extract::<AvgWinner>(py)?;
103                self.register_statistic(Arc::new(stat));
104            }
105            "MaxLoser" => {
106                let stat = statistic.extract::<MaxLoser>(py)?;
107                self.register_statistic(Arc::new(stat));
108            }
109            "MinLoser" => {
110                let stat = statistic.extract::<MinLoser>(py)?;
111                self.register_statistic(Arc::new(stat));
112            }
113            "AvgLoser" => {
114                let stat = statistic.extract::<AvgLoser>(py)?;
115                self.register_statistic(Arc::new(stat));
116            }
117            "Expectancy" => {
118                let stat = statistic.extract::<Expectancy>(py)?;
119                self.register_statistic(Arc::new(stat));
120            }
121            "WinRate" => {
122                let stat = statistic.extract::<WinRate>(py)?;
123                self.register_statistic(Arc::new(stat));
124            }
125            "ReturnsVolatility" => {
126                let stat = statistic.extract::<ReturnsVolatility>(py)?;
127                self.register_statistic(Arc::new(stat));
128            }
129            "ReturnsAverage" => {
130                let stat = statistic.extract::<ReturnsAverage>(py)?;
131                self.register_statistic(Arc::new(stat));
132            }
133            "ReturnsAverageLoss" => {
134                let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
135                self.register_statistic(Arc::new(stat));
136            }
137            "ReturnsAverageWin" => {
138                let stat = statistic.extract::<ReturnsAverageWin>(py)?;
139                self.register_statistic(Arc::new(stat));
140            }
141            "SharpeRatio" => {
142                let stat = statistic.extract::<SharpeRatio>(py)?;
143                self.register_statistic(Arc::new(stat));
144            }
145            "SortinoRatio" => {
146                let stat = statistic.extract::<SortinoRatio>(py)?;
147                self.register_statistic(Arc::new(stat));
148            }
149            "ProfitFactor" => {
150                let stat = statistic.extract::<ProfitFactor>(py)?;
151                self.register_statistic(Arc::new(stat));
152            }
153            "RiskReturnRatio" => {
154                let stat = statistic.extract::<RiskReturnRatio>(py)?;
155                self.register_statistic(Arc::new(stat));
156            }
157            "LongRatio" => {
158                let stat = statistic.extract::<LongRatio>(py)?;
159                self.register_statistic(Arc::new(stat));
160            }
161            _ => {
162                return Err(PyValueError::new_err(format!(
163                    "Unknown statistic type: {type_name}"
164                )));
165            }
166        }
167
168        Ok(())
169    }
170
171    #[pyo3(name = "deregister_statistic")]
172    fn py_deregister_statistic(&mut self, py: Python, statistic: Py<PyAny>) -> PyResult<()> {
173        let type_name = statistic
174            .getattr(py, "__class__")?
175            .getattr(py, "__name__")?
176            .extract::<String>(py)?;
177
178        match type_name.as_str() {
179            "MaxWinner" => {
180                let stat = statistic.extract::<MaxWinner>(py)?;
181                self.deregister_statistic(Arc::new(stat));
182            }
183            "MinWinner" => {
184                let stat = statistic.extract::<MinWinner>(py)?;
185                self.deregister_statistic(Arc::new(stat));
186            }
187            "AvgWinner" => {
188                let stat = statistic.extract::<AvgWinner>(py)?;
189                self.deregister_statistic(Arc::new(stat));
190            }
191            "MaxLoser" => {
192                let stat = statistic.extract::<MaxLoser>(py)?;
193                self.deregister_statistic(Arc::new(stat));
194            }
195            "MinLoser" => {
196                let stat = statistic.extract::<MinLoser>(py)?;
197                self.deregister_statistic(Arc::new(stat));
198            }
199            "AvgLoser" => {
200                let stat = statistic.extract::<AvgLoser>(py)?;
201                self.deregister_statistic(Arc::new(stat));
202            }
203            "Expectancy" => {
204                let stat = statistic.extract::<Expectancy>(py)?;
205                self.deregister_statistic(Arc::new(stat));
206            }
207            "WinRate" => {
208                let stat = statistic.extract::<WinRate>(py)?;
209                self.deregister_statistic(Arc::new(stat));
210            }
211            "ReturnsVolatility" => {
212                let stat = statistic.extract::<ReturnsVolatility>(py)?;
213                self.deregister_statistic(Arc::new(stat));
214            }
215            "ReturnsAverage" => {
216                let stat = statistic.extract::<ReturnsAverage>(py)?;
217                self.deregister_statistic(Arc::new(stat));
218            }
219            "ReturnsAverageLoss" => {
220                let stat = statistic.extract::<ReturnsAverageLoss>(py)?;
221                self.deregister_statistic(Arc::new(stat));
222            }
223            "ReturnsAverageWin" => {
224                let stat = statistic.extract::<ReturnsAverageWin>(py)?;
225                self.deregister_statistic(Arc::new(stat));
226            }
227            "SharpeRatio" => {
228                let stat = statistic.extract::<SharpeRatio>(py)?;
229                self.deregister_statistic(Arc::new(stat));
230            }
231            "SortinoRatio" => {
232                let stat = statistic.extract::<SortinoRatio>(py)?;
233                self.deregister_statistic(Arc::new(stat));
234            }
235            "ProfitFactor" => {
236                let stat = statistic.extract::<ProfitFactor>(py)?;
237                self.deregister_statistic(Arc::new(stat));
238            }
239            "RiskReturnRatio" => {
240                let stat = statistic.extract::<RiskReturnRatio>(py)?;
241                self.deregister_statistic(Arc::new(stat));
242            }
243            "LongRatio" => {
244                let stat = statistic.extract::<LongRatio>(py)?;
245                self.deregister_statistic(Arc::new(stat));
246            }
247            _ => {
248                return Err(PyValueError::new_err(format!(
249                    "Unknown statistic type: {type_name}"
250                )));
251            }
252        }
253
254        Ok(())
255    }
256
257    #[pyo3(name = "deregister_statistics")]
258    fn py_deregister_statistics(&mut self) {
259        self.deregister_statistics();
260    }
261
262    #[pyo3(name = "add_positions")]
263    fn py_add_positions(&mut self, py: Python, positions: Vec<Py<PyAny>>) -> PyResult<()> {
264        // Extract Position objects from Cython wrappers
265        let positions: Vec<Position> = positions
266            .iter()
267            .map(|p| {
268                // Try to get the underlying Rust Position
269                // For now, we'll need to handle Cython Position by accessing its _mem field
270                p.getattr(py, "_mem")?.extract::<Position>(py)
271            })
272            .collect::<PyResult<Vec<Position>>>()?;
273
274        self.add_positions(&positions);
275        Ok(())
276    }
277
278    #[pyo3(name = "add_trade")]
279    fn py_add_trade(&mut self, position_id: &PositionId, realized_pnl: &Money) {
280        self.add_trade(position_id, realized_pnl);
281    }
282
283    // Note: calculate_statistics is not exposed to Python because it requires
284    // complex conversions of Account and dict types. Use the Python analyzer.py wrapper instead.
285
286    #[pyo3(name = "statistic")]
287    fn py_statistic(&self, name: &str) -> Option<String> {
288        self.statistic(name).map(|s| s.name())
289    }
290
291    #[pyo3(name = "returns")]
292    fn py_returns(&self, py: Python) -> PyResult<Py<PyAny>> {
293        // Convert BTreeMap<UnixNanos, f64> to Python dict
294        let dict = pyo3::types::PyDict::new(py);
295        for (timestamp, value) in self.returns() {
296            dict.set_item(timestamp.as_u64(), value)?;
297        }
298        Ok(dict.into())
299    }
300
301    #[pyo3(name = "realized_pnls")]
302    fn py_realized_pnls(&self, py: Python, currency: Option<&Currency>) -> PyResult<Py<PyAny>> {
303        match self.realized_pnls(currency) {
304            Some(pnls) => {
305                // Convert Vec<(PositionId, f64)> to Python list of tuples or dict
306                let dict = pyo3::types::PyDict::new(py);
307                for (position_id, pnl) in pnls {
308                    dict.set_item(position_id.to_string(), pnl)?;
309                }
310                Ok(dict.into())
311            }
312            None => Ok(py.None()),
313        }
314    }
315
316    #[pyo3(name = "total_pnl")]
317    fn py_total_pnl(
318        &self,
319        currency: Option<&Currency>,
320        unrealized_pnl: Option<&Money>,
321    ) -> PyResult<f64> {
322        self.total_pnl(currency, unrealized_pnl)
323            .map_err(to_pyvalue_err)
324    }
325
326    #[pyo3(name = "total_pnl_percentage")]
327    fn py_total_pnl_percentage(
328        &self,
329        currency: Option<&Currency>,
330        unrealized_pnl: Option<&Money>,
331    ) -> PyResult<f64> {
332        self.total_pnl_percentage(currency, unrealized_pnl)
333            .map_err(to_pyvalue_err)
334    }
335
336    #[pyo3(name = "get_stats_pnls_formatted")]
337    fn py_get_stats_pnls_formatted(
338        &self,
339        currency: Option<&Currency>,
340        unrealized_pnl: Option<&Money>,
341    ) -> PyResult<Vec<String>> {
342        self.get_stats_pnls_formatted(currency, unrealized_pnl)
343            .map_err(PyValueError::new_err)
344    }
345
346    #[pyo3(name = "get_stats_returns_formatted")]
347    fn py_get_stats_returns_formatted(&self) -> Vec<String> {
348        self.get_stats_returns_formatted()
349    }
350
351    #[pyo3(name = "get_stats_general_formatted")]
352    fn py_get_stats_general_formatted(&self) -> Vec<String> {
353        self.get_stats_general_formatted()
354    }
355}