Skip to content

Commit f5fb3ce

Browse files
authored
Add small-account whole-share compatibility (#43)
1 parent df32c4b commit f5fb3ce

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Helpers for whole-share execution on small accounts."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Iterable, Mapping
6+
7+
8+
__all__ = ["project_unbuyable_value_targets_to_cash"]
9+
10+
11+
def _normalize_symbol(value: object) -> str:
12+
return str(value or "").strip().upper()
13+
14+
15+
def project_unbuyable_value_targets_to_cash(
16+
target_values: Mapping[str, object],
17+
prices: Mapping[str, object],
18+
*,
19+
symbols: Iterable[str] | None = None,
20+
quantity_step: float = 1.0,
21+
) -> tuple[dict[str, float], tuple[str, ...]]:
22+
"""Zero value targets that cannot buy one execution quantity step.
23+
24+
This keeps strategy output intact while letting whole-share execution layers
25+
avoid preserving a single oversized share for a sleeve whose target is less
26+
than one tradable unit.
27+
"""
28+
29+
adjusted = {
30+
_normalize_symbol(symbol): float(value or 0.0)
31+
for symbol, value in dict(target_values or {}).items()
32+
}
33+
step = max(0.0, float(quantity_step or 0.0))
34+
if step <= 0.0:
35+
return adjusted, ()
36+
37+
if symbols is None:
38+
candidate_symbols = tuple(adjusted)
39+
else:
40+
candidate_symbols = tuple(dict.fromkeys(_normalize_symbol(symbol) for symbol in symbols))
41+
42+
substituted: list[str] = []
43+
normalized_prices = {
44+
_normalize_symbol(symbol): float(price or 0.0)
45+
for symbol, price in dict(prices or {}).items()
46+
}
47+
for symbol in candidate_symbols:
48+
if not symbol:
49+
continue
50+
target_value = max(0.0, float(adjusted.get(symbol, 0.0) or 0.0))
51+
price = max(0.0, float(normalized_prices.get(symbol, 0.0) or 0.0))
52+
if price <= 0.0:
53+
continue
54+
if 0.0 < target_value < (price * step):
55+
adjusted[symbol] = 0.0
56+
substituted.append(symbol)
57+
58+
return adjusted, tuple(dict.fromkeys(substituted))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
3+
from quant_platform_kit.common.small_account_compatibility import (
4+
project_unbuyable_value_targets_to_cash,
5+
)
6+
7+
8+
class SmallAccountCompatibilityTests(unittest.TestCase):
9+
def test_projects_value_targets_below_one_share_to_cash(self):
10+
adjusted, substituted = project_unbuyable_value_targets_to_cash(
11+
{"SOXL": 541.58, "SOXX": 154.74, "BOXX": 0.0},
12+
{"SOXL": 191.15, "SOXX": 536.88, "BOXX": 100.0},
13+
)
14+
15+
self.assertEqual(adjusted["SOXL"], 541.58)
16+
self.assertEqual(adjusted["SOXX"], 0.0)
17+
self.assertEqual(adjusted["BOXX"], 0.0)
18+
self.assertEqual(substituted, ("SOXX",))
19+
20+
def test_keeps_targets_that_can_buy_one_quantity_step(self):
21+
adjusted, substituted = project_unbuyable_value_targets_to_cash(
22+
{"AAA": 100.0, "BBB": 99.99},
23+
{"AAA": 50.0, "BBB": 50.0},
24+
quantity_step=2.0,
25+
)
26+
27+
self.assertEqual(adjusted["AAA"], 100.0)
28+
self.assertEqual(adjusted["BBB"], 0.0)
29+
self.assertEqual(substituted, ("BBB",))
30+
31+
32+
if __name__ == "__main__":
33+
unittest.main()

0 commit comments

Comments
 (0)