Skip to content

Commit 0b88f24

Browse files
committed
Fix Calmar Ratio zero-drawdown convention; add INITIAL_BALANCE market override
- get_calmar_ratio now returns inf when max_drawdown is 0 and CAGR is positive (matching the Sortino/Profit Factor/Omega Ratio convention from #599/#600), instead of a flat 0.0 regardless of CAGR sign. - analyze_backtest_windows' inline Calmar computation aligned to match. - add_market() now supports <MARKET>_OVERRIDE_INITIAL_BALANCE, closing the same override gap that already existed for api_key/secret_key/ paper_trading/paper_trading_mode.
1 parent 39c8687 commit 0b88f24

6 files changed

Lines changed: 74 additions & 12 deletions

File tree

docusaurus/docs/Getting Started/portfolio-configuration.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,23 @@ BITVAVO_OVERRIDE_API_KEY=<sandbox-or-live-api-key>
118118
BITVAVO_OVERRIDE_SECRET_KEY=<sandbox-or-live-secret>
119119
BITVAVO_OVERRIDE_PAPER_TRADING=true
120120
BITVAVO_OVERRIDE_PAPER_TRADING_MODE=local
121+
BITVAVO_OVERRIDE_INITIAL_BALANCE=1000
121122
```
122123

123124
Unlike the fallback variables above, these **replace** the corresponding
124125
`add_market()` argument whenever set, regardless of what was passed:
125126
`<MARKET>_OVERRIDE_API_KEY` and `<MARKET>_OVERRIDE_SECRET_KEY` override
126-
`api_key`/`secret_key`, and `<MARKET>_OVERRIDE_PAPER_TRADING`/
127+
`api_key`/`secret_key`, `<MARKET>_OVERRIDE_PAPER_TRADING`/
127128
`<MARKET>_OVERRIDE_PAPER_TRADING_MODE` override `paper_trading`/
128-
`paper_trading_mode`. The prefix comes from the market already selected by
129-
the call — these variables never redirect `market` or `trading_symbol`
130-
themselves.
129+
`paper_trading_mode`, and `<MARKET>_OVERRIDE_INITIAL_BALANCE` overrides
130+
`initial_balance` — including a value hardcoded in an entry script. The
131+
prefix comes from the market already selected by the call — these
132+
variables never redirect `market` or `trading_symbol` themselves.
131133

132134
Paper-trading booleans accept `true`/`false`, `yes`/`no`, `on`/`off`, and
133135
`1`/`0`, case-insensitively. Modes accept `auto`, `broker`, and `local`,
134-
also case-insensitively. Invalid values fail fast with
135-
`ImproperlyConfigured`.
136+
also case-insensitively. `INITIAL_BALANCE` must parse as a number.
137+
Invalid values fail fast with `ImproperlyConfigured`.
136138

137139
These overrides apply only to `add_market()`. Directly constructed
138140
`PortfolioConfiguration` objects only read the general fallback variables

investing_algorithm_framework/analysis/backtest_window_analysis.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ def analyze_backtest_windows(
177177
) * 100
178178
calmar = (
179179
annual_return / abs(max_drawdown)
180-
if max_drawdown < 0 else 0.0
180+
if max_drawdown < 0
181+
# No drawdown at all. Same convention as get_calmar_ratio,
182+
# get_sortino_ratio, get_profit_factor and get_omega_ratio.
183+
else (float('inf') if annual_return > 0 else 0.0)
181184
)
182185

183186
skew = float(pct.skew()) if len(pct) > 2 else float("nan")

investing_algorithm_framework/app/app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ def _parse_environment_boolean(variable_name, fallback):
6464
)
6565

6666

67+
def _parse_environment_float(variable_name, fallback):
68+
value = os.getenv(variable_name)
69+
70+
if value is None:
71+
return fallback
72+
73+
try:
74+
return float(value)
75+
except ValueError:
76+
raise ImproperlyConfigured(
77+
f"{variable_name} environment variable {value!r} is not a "
78+
f"valid number"
79+
)
80+
81+
6782
def _build_strategy_universe_map(strategies, universe):
6883
"""Thin wrapper around the domain helper of the same name; kept for
6984
backwards compatibility with code paths inside ``app.py``."""
@@ -2505,7 +2520,8 @@ def add_market(
25052520
initialization.
25062521
initial_balance: Initial balance for the market. Falls
25072522
back to the ``INITIAL_BALANCE`` environment variable
2508-
when not given.
2523+
when not given. Overridden by
2524+
``{MARKET}_OVERRIDE_INITIAL_BALANCE`` when set.
25092525
fee_percentage: Default fee percentage for all trades
25102526
on this market (e.g. 0.1 for 0.1%). Can be overridden
25112527
per-symbol via TradingCost on the strategy.
@@ -2546,6 +2562,10 @@ def add_market(
25462562
f"{environment_prefix}_OVERRIDE_PAPER_TRADING",
25472563
paper_trading,
25482564
)
2565+
initial_balance = _parse_environment_float(
2566+
f"{environment_prefix}_OVERRIDE_INITIAL_BALANCE",
2567+
initial_balance,
2568+
)
25492569

25502570
paper_trading_mode_override = os.getenv(
25512571
f"{environment_prefix}_OVERRIDE_PAPER_TRADING_MODE"

investing_algorithm_framework/services/metrics/calmar_ratio.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ def get_calmar_ratio(snapshots: List[PortfolioSnapshot]):
2929
from the backtest report.
3030
3131
Returns:
32-
float: The Calmar Ratio.
32+
float: The Calmar Ratio. Returns ``float('inf')`` when there was
33+
no drawdown at all but CAGR is positive, and ``0.0`` when
34+
there is no drawdown and no positive return (mirrors the
35+
division-by-zero convention used by ``get_sortino_ratio``,
36+
``get_profit_factor`` and ``get_omega_ratio``).
3337
"""
3438
cagr = get_cagr(snapshots)
3539
max_drawdown = get_max_drawdown(snapshots)
3640

3741
if max_drawdown == 0 or max_drawdown is None:
38-
return 0.0
42+
return float('inf') if cagr > 0 else 0.0
3943

4044
return cagr / max_drawdown

tests/app/test_add_market.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,35 @@ def test_false_paper_trading_environment_overrides_true_argument(self):
139139
configuration = app.get_portfolio_configurations()[0]
140140
self.assertFalse(configuration.paper_trading)
141141

142+
def test_initial_balance_override_replaces_argument(self):
143+
with patch.dict(
144+
"os.environ",
145+
{"BINANCE_OVERRIDE_INITIAL_BALANCE": "1000"},
146+
clear=False,
147+
):
148+
app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
149+
app.add_market(
150+
market="binance",
151+
trading_symbol="EUR",
152+
initial_balance=400,
153+
)
154+
155+
configuration = app.get_portfolio_configurations()[0]
156+
self.assertEqual(1000.0, configuration.initial_balance)
157+
158+
def test_invalid_initial_balance_override_fails_fast(self):
159+
with patch.dict(
160+
"os.environ",
161+
{"BINANCE_OVERRIDE_INITIAL_BALANCE": "not-a-number"},
162+
clear=False,
163+
):
164+
app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
165+
with self.assertRaisesRegex(
166+
ImproperlyConfigured,
167+
"BINANCE_OVERRIDE_INITIAL_BALANCE",
168+
):
169+
app.add_market(market="binance", trading_symbol="EUR")
170+
142171
def test_market_and_trading_symbol_arguments_are_not_overridden(self):
143172
with patch.dict(
144173
"os.environ",

tests/services/metrics/test_calmer_ratio.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,18 @@ def test_typical_case(self):
6262
def test_calmar_ratio_zero_drawdown(self):
6363
"""
6464
Test Calmar ratio when there are no drawdowns (only gains).
65-
Should return 0.0 since we can't divide by zero.
65+
66+
There is no downside risk to divide by and CAGR is positive, so
67+
the ratio is unbounded. This mirrors the division-by-zero
68+
convention used by get_sortino_ratio, get_profit_factor and
69+
get_omega_ratio.
6670
"""
6771
report = self._create_report(
6872
[1000, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000],
6973
[datetime(2024, 1, i) for i in range(1, 11)]
7074
)
7175
ratio = get_calmar_ratio(report.portfolio_snapshots)
72-
self.assertEqual(ratio, 0.0)
76+
self.assertEqual(ratio, float('inf'))
7377

7478
def test_calmar_ratio_with_only_drawdown(self):
7579
"""

0 commit comments

Comments
 (0)