From c7640a30d7e2d5ebde7d235e72cb09dc99f619f4 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 20 Feb 2026 08:29:09 +0900 Subject: [PATCH] feat: use playbook allocation_pct in position sizing (#172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add playbook_allocation_pct and scenario_confidence parameters to _determine_order_quantity() with playbook-based sizing taking priority over volatility-score fallback when provided - Confidence scaling: confidence/80 multiplier (confidence 96 → 1.2x) clipped to [POSITION_MIN_ALLOCATION_PCT, POSITION_MAX_ALLOCATION_PCT] - Pass matched_scenario.allocation_pct and match.confidence from trading_cycle so AI's allocation decisions reach order execution - Add 4 new tests: playbook priority, confidence scaling, max clamp, and fallback behavior Closes #172 Co-Authored-By: Claude Sonnet 4.6 --- src/main.py | 28 ++++++++++++++++- tests/test_main.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 58bd045..f160f11 100644 --- a/src/main.py +++ b/src/main.py @@ -190,8 +190,15 @@ def _determine_order_quantity( candidate: ScanCandidate | None, settings: Settings | None, broker_held_qty: int = 0, + playbook_allocation_pct: float | None = None, + scenario_confidence: int = 80, ) -> int: - """Determine order quantity using volatility-aware position sizing.""" + """Determine order quantity using volatility-aware position sizing. + + Priority: + 1. playbook_allocation_pct (AI-specified) scaled by scenario_confidence + 2. Fallback: volatility-score-based allocation from scanner candidate + """ if action == "SELL": return broker_held_qty if current_price <= 0 or total_cash <= 0: @@ -200,6 +207,22 @@ def _determine_order_quantity( if settings is None or not settings.POSITION_SIZING_ENABLED: return 1 + # Use AI-specified allocation_pct if available + if playbook_allocation_pct is not None: + # Confidence scaling: confidence 80 → 1.0x, confidence 95 → 1.19x + confidence_scale = scenario_confidence / 80.0 + effective_pct = min( + settings.POSITION_MAX_ALLOCATION_PCT, + max( + settings.POSITION_MIN_ALLOCATION_PCT, + playbook_allocation_pct * confidence_scale, + ), + ) + budget = total_cash * (effective_pct / 100.0) + quantity = int(budget // current_price) + return max(0, quantity) + + # Fallback: volatility-score-based allocation target_score = max(1.0, settings.POSITION_VOLATILITY_TARGET_SCORE) observed_score = candidate.score if candidate else target_score observed_score = max(1.0, min(100.0, observed_score)) @@ -568,6 +591,7 @@ async def trading_cycle( if decision.action == "SELL" else 0 ) + matched_scenario = match.matched_scenario quantity = _determine_order_quantity( action=decision.action, current_price=current_price, @@ -575,6 +599,8 @@ async def trading_cycle( candidate=candidate, settings=settings, broker_held_qty=broker_held_qty, + playbook_allocation_pct=matched_scenario.allocation_pct if matched_scenario else None, + scenario_confidence=match.confidence, ) if quantity <= 0: logger.info( diff --git a/tests/test_main.py b/tests/test_main.py index ef95c14..13ae917 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -205,6 +205,84 @@ class TestDetermineOrderQuantity: ) assert result == 2 + def test_determine_order_quantity_uses_playbook_allocation_pct(self) -> None: + """playbook_allocation_pct should take priority over volatility-based sizing.""" + settings = MagicMock(spec=Settings) + settings.POSITION_SIZING_ENABLED = True + settings.POSITION_MAX_ALLOCATION_PCT = 30.0 + settings.POSITION_MIN_ALLOCATION_PCT = 1.0 + # playbook says 20%, confidence 80 → scale=1.0 → 20% + # 1,000,000 * 20% = 200,000 // 50,000 price = 4 shares + result = _determine_order_quantity( + action="BUY", + current_price=50000.0, + total_cash=1000000.0, + candidate=None, + settings=settings, + playbook_allocation_pct=20.0, + scenario_confidence=80, + ) + assert result == 4 + + def test_determine_order_quantity_confidence_scales_allocation(self) -> None: + """Higher confidence should produce a larger allocation (up to max).""" + settings = MagicMock(spec=Settings) + settings.POSITION_SIZING_ENABLED = True + settings.POSITION_MAX_ALLOCATION_PCT = 30.0 + settings.POSITION_MIN_ALLOCATION_PCT = 1.0 + # confidence 96 → scale=1.2 → 10% * 1.2 = 12% + # 1,000,000 * 12% = 120,000 // 50,000 price = 2 shares + result = _determine_order_quantity( + action="BUY", + current_price=50000.0, + total_cash=1000000.0, + candidate=None, + settings=settings, + playbook_allocation_pct=10.0, + scenario_confidence=96, + ) + # scale = 96/80 = 1.2 → effective_pct = 12.0 + # budget = 1_000_000 * 0.12 = 120_000 → qty = 120_000 // 50_000 = 2 + assert result == 2 + + def test_determine_order_quantity_confidence_clamped_to_max(self) -> None: + """Confidence scaling should not exceed POSITION_MAX_ALLOCATION_PCT.""" + settings = MagicMock(spec=Settings) + settings.POSITION_SIZING_ENABLED = True + settings.POSITION_MAX_ALLOCATION_PCT = 15.0 + settings.POSITION_MIN_ALLOCATION_PCT = 1.0 + # playbook 20% * scale 1.5 = 30% → clamped to 15% + # 1,000,000 * 15% = 150,000 // 50,000 price = 3 shares + result = _determine_order_quantity( + action="BUY", + current_price=50000.0, + total_cash=1000000.0, + candidate=None, + settings=settings, + playbook_allocation_pct=20.0, + scenario_confidence=120, # extreme → scale = 1.5 + ) + assert result == 3 + + def test_determine_order_quantity_fallback_when_no_playbook(self) -> None: + """Without playbook_allocation_pct, falls back to volatility-based sizing.""" + settings = MagicMock(spec=Settings) + settings.POSITION_SIZING_ENABLED = True + settings.POSITION_VOLATILITY_TARGET_SCORE = 50.0 + settings.POSITION_BASE_ALLOCATION_PCT = 10.0 + settings.POSITION_MAX_ALLOCATION_PCT = 30.0 + settings.POSITION_MIN_ALLOCATION_PCT = 1.0 + # Same as test_buy_with_position_sizing_calculates_correctly (no playbook) + result = _determine_order_quantity( + action="BUY", + current_price=50000.0, + total_cash=1000000.0, + candidate=None, + settings=settings, + playbook_allocation_pct=None, # explicit None → fallback + ) + assert result == 2 + class TestSafeFloat: """Test safe_float() helper function.""" -- 2.49.1