From 9131a7f36497da2cac4b78af79a5638537e5aa1d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:38:34 +0800 Subject: [PATCH] Add broker cost order floor helper --- src/quant_platform_kit/common/broker_costs.py | 54 +++++++++++++++++++ tests/test_broker_costs.py | 48 +++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/quant_platform_kit/common/broker_costs.py create mode 100644 tests/test_broker_costs.py diff --git a/src/quant_platform_kit/common/broker_costs.py b/src/quant_platform_kit/common/broker_costs.py new file mode 100644 index 0000000..4e7ee52 --- /dev/null +++ b/src/quant_platform_kit/common/broker_costs.py @@ -0,0 +1,54 @@ +"""Broker execution cost helpers.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass + + +__all__ = [ + "BrokerCostProfile", + "minimum_economic_order_notional_usd", +] + + +@dataclass(frozen=True) +class BrokerCostProfile: + """Small account broker cost inputs for economic order filtering.""" + + fixed_order_fee_usd: float = 0.0 + minimum_order_fee_usd: float = 0.0 + max_fixed_fee_bps: float = 100.0 + explicit_min_order_notional_usd: float = 0.0 + + +def _non_negative_finite(value: object, *, default: float = 0.0) -> float: + try: + numeric = float(value or 0.0) + except (TypeError, ValueError): + return float(default) + if not math.isfinite(numeric): + return float(default) + return max(0.0, numeric) + + +def minimum_economic_order_notional_usd(profile: BrokerCostProfile | None) -> float: + """Return the minimum order notional implied by fixed order costs. + + The helper intentionally models only fixed or minimum per-order fees. Per-share + fees and sell-side regulatory fees do not produce a stable notional floor and + should be handled by cost reporting/backtests rather than blocking risk exits. + """ + + if profile is None: + return 0.0 + explicit_floor = _non_negative_finite(profile.explicit_min_order_notional_usd) + fee_floor = max( + _non_negative_finite(profile.fixed_order_fee_usd), + _non_negative_finite(profile.minimum_order_fee_usd), + ) + max_fee_bps = _non_negative_finite(profile.max_fixed_fee_bps) + if fee_floor <= 0.0 or max_fee_bps <= 0.0: + return explicit_floor + implied_floor = fee_floor / (max_fee_bps / 10_000.0) + return max(explicit_floor, implied_floor) diff --git a/tests/test_broker_costs.py b/tests/test_broker_costs.py new file mode 100644 index 0000000..14b242f --- /dev/null +++ b/tests/test_broker_costs.py @@ -0,0 +1,48 @@ +import unittest + +from quant_platform_kit.common.broker_costs import ( + BrokerCostProfile, + minimum_economic_order_notional_usd, +) + + +class BrokerCostTests(unittest.TestCase): + def test_zero_fixed_fee_keeps_explicit_floor(self): + self.assertEqual( + minimum_economic_order_notional_usd( + BrokerCostProfile(explicit_min_order_notional_usd=25.0) + ), + 25.0, + ) + + def test_minimum_order_fee_sets_economic_floor(self): + self.assertEqual( + minimum_economic_order_notional_usd( + BrokerCostProfile(minimum_order_fee_usd=0.35, max_fixed_fee_bps=50.0) + ), + 70.0, + ) + + def test_longbridge_like_fixed_fee_uses_bps_limit(self): + self.assertEqual( + minimum_economic_order_notional_usd( + BrokerCostProfile(fixed_order_fee_usd=0.99, max_fixed_fee_bps=100.0) + ), + 99.0, + ) + + def test_explicit_floor_can_be_more_conservative_than_fee_floor(self): + self.assertEqual( + minimum_economic_order_notional_usd( + BrokerCostProfile( + fixed_order_fee_usd=0.99, + max_fixed_fee_bps=100.0, + explicit_min_order_notional_usd=150.0, + ) + ), + 150.0, + ) + + +if __name__ == "__main__": + unittest.main()