Skip to content

Commit c6e221f

Browse files
authored
Add broker cost order floor helper (#92)
1 parent ac9277c commit c6e221f

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Broker execution cost helpers."""
2+
3+
from __future__ import annotations
4+
5+
import math
6+
from dataclasses import dataclass
7+
8+
9+
__all__ = [
10+
"BrokerCostProfile",
11+
"minimum_economic_order_notional_usd",
12+
]
13+
14+
15+
@dataclass(frozen=True)
16+
class BrokerCostProfile:
17+
"""Small account broker cost inputs for economic order filtering."""
18+
19+
fixed_order_fee_usd: float = 0.0
20+
minimum_order_fee_usd: float = 0.0
21+
max_fixed_fee_bps: float = 100.0
22+
explicit_min_order_notional_usd: float = 0.0
23+
24+
25+
def _non_negative_finite(value: object, *, default: float = 0.0) -> float:
26+
try:
27+
numeric = float(value or 0.0)
28+
except (TypeError, ValueError):
29+
return float(default)
30+
if not math.isfinite(numeric):
31+
return float(default)
32+
return max(0.0, numeric)
33+
34+
35+
def minimum_economic_order_notional_usd(profile: BrokerCostProfile | None) -> float:
36+
"""Return the minimum order notional implied by fixed order costs.
37+
38+
The helper intentionally models only fixed or minimum per-order fees. Per-share
39+
fees and sell-side regulatory fees do not produce a stable notional floor and
40+
should be handled by cost reporting/backtests rather than blocking risk exits.
41+
"""
42+
43+
if profile is None:
44+
return 0.0
45+
explicit_floor = _non_negative_finite(profile.explicit_min_order_notional_usd)
46+
fee_floor = max(
47+
_non_negative_finite(profile.fixed_order_fee_usd),
48+
_non_negative_finite(profile.minimum_order_fee_usd),
49+
)
50+
max_fee_bps = _non_negative_finite(profile.max_fixed_fee_bps)
51+
if fee_floor <= 0.0 or max_fee_bps <= 0.0:
52+
return explicit_floor
53+
implied_floor = fee_floor / (max_fee_bps / 10_000.0)
54+
return max(explicit_floor, implied_floor)

tests/test_broker_costs.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import unittest
2+
3+
from quant_platform_kit.common.broker_costs import (
4+
BrokerCostProfile,
5+
minimum_economic_order_notional_usd,
6+
)
7+
8+
9+
class BrokerCostTests(unittest.TestCase):
10+
def test_zero_fixed_fee_keeps_explicit_floor(self):
11+
self.assertEqual(
12+
minimum_economic_order_notional_usd(
13+
BrokerCostProfile(explicit_min_order_notional_usd=25.0)
14+
),
15+
25.0,
16+
)
17+
18+
def test_minimum_order_fee_sets_economic_floor(self):
19+
self.assertEqual(
20+
minimum_economic_order_notional_usd(
21+
BrokerCostProfile(minimum_order_fee_usd=0.35, max_fixed_fee_bps=50.0)
22+
),
23+
70.0,
24+
)
25+
26+
def test_longbridge_like_fixed_fee_uses_bps_limit(self):
27+
self.assertEqual(
28+
minimum_economic_order_notional_usd(
29+
BrokerCostProfile(fixed_order_fee_usd=0.99, max_fixed_fee_bps=100.0)
30+
),
31+
99.0,
32+
)
33+
34+
def test_explicit_floor_can_be_more_conservative_than_fee_floor(self):
35+
self.assertEqual(
36+
minimum_economic_order_notional_usd(
37+
BrokerCostProfile(
38+
fixed_order_fee_usd=0.99,
39+
max_fixed_fee_bps=100.0,
40+
explicit_min_order_notional_usd=150.0,
41+
)
42+
),
43+
150.0,
44+
)
45+
46+
47+
if __name__ == "__main__":
48+
unittest.main()

0 commit comments

Comments
 (0)