Compare commits
2 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7640a30d7 | ||
| 03f8d220a4 |
28
src/main.py
28
src/main.py
@@ -190,8 +190,15 @@ def _determine_order_quantity(
|
|||||||
candidate: ScanCandidate | None,
|
candidate: ScanCandidate | None,
|
||||||
settings: Settings | None,
|
settings: Settings | None,
|
||||||
broker_held_qty: int = 0,
|
broker_held_qty: int = 0,
|
||||||
|
playbook_allocation_pct: float | None = None,
|
||||||
|
scenario_confidence: int = 80,
|
||||||
) -> int:
|
) -> 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":
|
if action == "SELL":
|
||||||
return broker_held_qty
|
return broker_held_qty
|
||||||
if current_price <= 0 or total_cash <= 0:
|
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:
|
if settings is None or not settings.POSITION_SIZING_ENABLED:
|
||||||
return 1
|
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)
|
target_score = max(1.0, settings.POSITION_VOLATILITY_TARGET_SCORE)
|
||||||
observed_score = candidate.score if candidate else target_score
|
observed_score = candidate.score if candidate else target_score
|
||||||
observed_score = max(1.0, min(100.0, observed_score))
|
observed_score = max(1.0, min(100.0, observed_score))
|
||||||
@@ -568,6 +591,7 @@ async def trading_cycle(
|
|||||||
if decision.action == "SELL"
|
if decision.action == "SELL"
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
|
matched_scenario = match.matched_scenario
|
||||||
quantity = _determine_order_quantity(
|
quantity = _determine_order_quantity(
|
||||||
action=decision.action,
|
action=decision.action,
|
||||||
current_price=current_price,
|
current_price=current_price,
|
||||||
@@ -575,6 +599,8 @@ async def trading_cycle(
|
|||||||
candidate=candidate,
|
candidate=candidate,
|
||||||
settings=settings,
|
settings=settings,
|
||||||
broker_held_qty=broker_held_qty,
|
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:
|
if quantity <= 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -205,6 +205,84 @@ class TestDetermineOrderQuantity:
|
|||||||
)
|
)
|
||||||
assert result == 2
|
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:
|
class TestSafeFloat:
|
||||||
"""Test safe_float() helper function."""
|
"""Test safe_float() helper function."""
|
||||||
|
|||||||
Reference in New Issue
Block a user