fix: use smart rule-based fallback playbook when Gemini fails (issue #145)
Some checks failed
CI / test (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
When gemini-2.5-flash quota is exhausted (20 RPD free tier), generate_playbook() fell back to _defensive_playbook() which only had price_change_pct_below: -3.0 SELL conditions — no BUY conditions — causing zero trades on US market despite scanner finding strong momentum/oversold candidates. Changes: - Add _smart_fallback_playbook() that uses scanner signals to build BUY conditions: - momentum signal: BUY when volume_ratio_above=VOL_MULTIPLIER - oversold signal: BUY when rsi_below=RSI_OVERSOLD_THRESHOLD - always: SELL stop-loss at price_change_pct_below=-3.0 - Use _smart_fallback_playbook() instead of _defensive_playbook() on Gemini failure - Add 10 new tests for _smart_fallback_playbook() covering momentum/oversold/empty cases - Update existing test_gemini_failure_returns_defensive to match new behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""Pre-market planner — generates DayPlaybook via Gemini before market open.
|
||||
|
||||
One Gemini API call per market per day. Candidates come from SmartVolatilityScanner.
|
||||
On failure, returns a defensive playbook (all HOLD, no trades).
|
||||
On failure, returns a smart rule-based fallback playbook that uses scanner signals
|
||||
(momentum/oversold) to generate BUY conditions, avoiding the all-HOLD problem.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -134,7 +135,7 @@ class PreMarketPlanner:
|
||||
except Exception:
|
||||
logger.exception("Playbook generation failed for %s", market)
|
||||
if self._settings.DEFENSIVE_PLAYBOOK_ON_FAILURE:
|
||||
return self._defensive_playbook(today, market, candidates)
|
||||
return self._smart_fallback_playbook(today, market, candidates, self._settings)
|
||||
return self._empty_playbook(today, market)
|
||||
|
||||
def build_cross_market_context(
|
||||
@@ -470,3 +471,99 @@ class PreMarketPlanner:
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _smart_fallback_playbook(
|
||||
today: date,
|
||||
market: str,
|
||||
candidates: list[ScanCandidate],
|
||||
settings: Settings,
|
||||
) -> DayPlaybook:
|
||||
"""Rule-based fallback playbook when Gemini is unavailable.
|
||||
|
||||
Uses scanner signals (RSI, volume_ratio) to generate meaningful BUY
|
||||
conditions instead of the all-SELL defensive playbook. Candidates are
|
||||
already pre-qualified by SmartVolatilityScanner, so we trust their
|
||||
signals and build actionable scenarios from them.
|
||||
|
||||
Scenario logic per candidate:
|
||||
- momentum signal: BUY when volume_ratio exceeds scanner threshold
|
||||
- oversold signal: BUY when RSI is below oversold threshold
|
||||
- always: SELL stop-loss at -3.0% as guard
|
||||
"""
|
||||
stock_playbooks = []
|
||||
for c in candidates:
|
||||
scenarios: list[StockScenario] = []
|
||||
|
||||
if c.signal == "momentum":
|
||||
scenarios.append(
|
||||
StockScenario(
|
||||
condition=StockCondition(
|
||||
volume_ratio_above=settings.VOL_MULTIPLIER,
|
||||
),
|
||||
action=ScenarioAction.BUY,
|
||||
confidence=80,
|
||||
allocation_pct=10.0,
|
||||
stop_loss_pct=-3.0,
|
||||
take_profit_pct=5.0,
|
||||
rationale=(
|
||||
f"Rule-based BUY: momentum signal, "
|
||||
f"volume={c.volume_ratio:.1f}x (fallback planner)"
|
||||
),
|
||||
)
|
||||
)
|
||||
elif c.signal == "oversold":
|
||||
scenarios.append(
|
||||
StockScenario(
|
||||
condition=StockCondition(
|
||||
rsi_below=settings.RSI_OVERSOLD_THRESHOLD,
|
||||
),
|
||||
action=ScenarioAction.BUY,
|
||||
confidence=80,
|
||||
allocation_pct=10.0,
|
||||
stop_loss_pct=-3.0,
|
||||
take_profit_pct=5.0,
|
||||
rationale=(
|
||||
f"Rule-based BUY: oversold signal, "
|
||||
f"RSI={c.rsi:.0f} (fallback planner)"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Always add stop-loss guard
|
||||
scenarios.append(
|
||||
StockScenario(
|
||||
condition=StockCondition(price_change_pct_below=-3.0),
|
||||
action=ScenarioAction.SELL,
|
||||
confidence=90,
|
||||
stop_loss_pct=-3.0,
|
||||
rationale="Rule-based stop-loss (fallback planner)",
|
||||
)
|
||||
)
|
||||
|
||||
stock_playbooks.append(
|
||||
StockPlaybook(
|
||||
stock_code=c.stock_code,
|
||||
scenarios=scenarios,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Smart fallback playbook for %s: %d stocks with rule-based BUY/SELL conditions",
|
||||
market,
|
||||
len(stock_playbooks),
|
||||
)
|
||||
return DayPlaybook(
|
||||
date=today,
|
||||
market=market,
|
||||
market_outlook=MarketOutlook.NEUTRAL,
|
||||
default_action=ScenarioAction.HOLD,
|
||||
stock_playbooks=stock_playbooks,
|
||||
global_rules=[
|
||||
GlobalRule(
|
||||
condition="portfolio_pnl_pct < -2.0",
|
||||
action=ScenarioAction.REDUCE_ALL,
|
||||
rationale="Defensive: reduce on loss threshold",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user