Skip to content

Commit a283561

Browse files
authored
Merge pull request #131 from QuantStrategyLab/feat/dca-compat-mode
feat: DCA compat mode
2 parents 247911c + e038cde commit a283561

3 files changed

Lines changed: 50 additions & 2 deletions

File tree

src/quant_platform_kit/common/execution_capabilities.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
FRACTIONAL_SHARE_EXECUTION_CAPABILITY = "fractional_share_execution"
66
FRACTIONAL_SHARE_EXECUTION_SKIP_REASON = "fractional_share_execution_required"
77

8+
# When a platform does NOT natively support fractional shares, DCA / notional
9+
# strategies can still run by converting each notional buy into a minimum
10+
# 1-share (US) or 1-lot (HK) order. This compat mode is signalled via
11+
# ``notional_buy_compat_mode_enabled()`` so the execution layer knows to
12+
# floor-up notional amounts instead of placing true fractional orders.
13+
NOTIONAL_TO_WHOLE_SHARE_COMPAT_SKIP_REASON = "notional_to_whole_share_compat"
14+
815

916
def definition_requires_fractional_share_execution(definition: StrategyDefinition) -> bool:
1017
return FRACTIONAL_SHARE_EXECUTION_CAPABILITY in frozenset(definition.compatible_capabilities)
@@ -20,6 +27,15 @@ def fractional_share_execution_unsupported_reason(
2027
strategy_catalog: StrategyCatalog,
2128
capability_matrix: PlatformCapabilityMatrix,
2229
) -> str | None:
30+
"""Return a reason string if *profile* requires fractional shares but the
31+
platform does **not** support them natively.
32+
33+
When the platform lacks ``fractional_share_execution`` but a strategy
34+
still needs to place notional orders, callers should check
35+
``notional_buy_compat_mode_enabled()`` — if that returns ``True`` the
36+
execution layer should convert each notional buy into a minimum
37+
whole-share (or whole-lot) order instead of skipping the strategy.
38+
"""
2339
normalized_profile = normalize_profile_name(profile)
2440
definition = strategy_catalog.definitions.get(normalized_profile)
2541
if definition is None:
@@ -28,3 +44,25 @@ def fractional_share_execution_unsupported_reason(
2844
if not platform_supports_fractional_share_execution(capability_matrix=capability_matrix):
2945
return FRACTIONAL_SHARE_EXECUTION_SKIP_REASON
3046
return None
47+
48+
49+
def notional_buy_compat_mode_enabled(
50+
profile: str,
51+
*,
52+
strategy_catalog: StrategyCatalog,
53+
capability_matrix: PlatformCapabilityMatrix,
54+
) -> bool:
55+
"""Return ``True`` when *profile* requires fractional execution but the
56+
platform does **not** support it natively.
57+
58+
In compat mode the execution layer should convert each notional buy
59+
intent into a minimum 1‑share (US) or 1‑lot (HK) order, falling back
60+
to skipping the order if the notional amount is smaller than one unit.
61+
"""
62+
normalized_profile = normalize_profile_name(profile)
63+
definition = strategy_catalog.definitions.get(normalized_profile)
64+
if definition is None:
65+
return False
66+
if not definition_requires_fractional_share_execution(definition):
67+
return False
68+
return not platform_supports_fractional_share_execution(capability_matrix=capability_matrix)

src/quant_platform_kit/common/strategies.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,14 @@ def _matches_platform_capability_matrix(
384384
adapter_capabilities = frozenset(runtime_adapter.available_capabilities)
385385
if definition.compatible_capabilities - adapter_capabilities:
386386
return False
387-
if adapter_capabilities - capability_matrix.supported_capabilities:
387+
# ``fractional_share_execution`` is a *soft* capability: when a platform
388+
# does not natively support it, the execution layer can still run the
389+
# strategy in compat mode by converting notional buys to minimum
390+
# whole-share / whole-lot orders. Drop only that capability before
391+
# checking the platform matrix so DCA profiles remain eligible.
392+
_soft_caps = frozenset({"fractional_share_execution"})
393+
_hard_adapter_caps = adapter_capabilities - _soft_caps
394+
if _hard_adapter_caps - capability_matrix.supported_capabilities:
388395
return False
389396

390397
return True

tests/test_execution_capabilities.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ def test_capability_matrix_excludes_fractional_dca_profiles(self) -> None:
109109
runtime_adapter_loader=lambda profile: adapters[profile],
110110
)
111111

112-
self.assertEqual(eligible, frozenset())
112+
# ``fractional_share_execution`` is a soft capability — profiles that
113+
# require it are still eligible even when the platform lacks native
114+
# support (compat mode converts notional → whole-share orders).
115+
self.assertEqual(eligible, frozenset({"ibit_smart_dca"}))
113116

114117

115118
if __name__ == "__main__":

0 commit comments

Comments
 (0)