Coverage for src / monte_neo / metrics / profit_factor.py: 60%
25 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
1"""Profit Factor metric.
3Calculates the ratio of gross profits to gross losses.
4"""
6from __future__ import annotations
8import numpy as np
10from monte_neo.utils.logger import get_logger
12logger = get_logger(__name__)
15class ProfitFactorMetric:
16 """Profit Factor calculator."""
18 def calculate(self, pnls: list[float] | np.ndarray) -> float:
19 """Calculate profit factor.
21 Profit Factor = Gross Profit / Gross Loss
23 Values > 1 indicate profitability.
24 Values > 2 are considered excellent.
26 Args:
27 pnls: List of P&L values.
29 Returns:
30 Profit factor value.
31 """
32 if not len(pnls):
33 return 0.0
35 pnls = np.array(pnls)
37 gross_profit = np.sum(pnls[pnls > 0])
38 gross_loss = abs(np.sum(pnls[pnls < 0]))
40 if gross_loss == 0:
41 return float("inf") if gross_profit > 0 else 0.0
43 return float(gross_profit / gross_loss)
45 def calculate_rolling(
46 self,
47 pnls: list[float] | np.ndarray,
48 window: int = 20,
49 ) -> np.ndarray:
50 """Calculate rolling profit factor.
52 Args:
53 pnls: List of P&L values.
54 window: Rolling window size.
56 Returns:
57 Array of rolling profit factors.
58 """
59 if len(pnls) < window:
60 return np.array([self.calculate(pnls)])
62 pnls = np.array(pnls)
63 rolling_pf = []
65 for i in range(window, len(pnls) + 1):
66 window_pnls = pnls[i - window : i]
67 rolling_pf.append(self.calculate(window_pnls))
69 return np.array(rolling_pf)
71 def is_acceptable(
72 self,
73 pnls: list[float] | np.ndarray,
74 threshold: float = 1.5,
75 ) -> bool:
76 """Check if profit factor meets threshold.
78 Args:
79 pnls: List of P&L values.
80 threshold: Minimum acceptable profit factor.
82 Returns:
83 True if profit factor >= threshold.
84 """
85 return self.calculate(pnls) >= threshold