feat(kevin): mention-driven backtest mini-engine
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Walks mentions chronologically, T+1 entry, time-based exit per KevinStrategy. Reuses backtester/metrics::compute_metrics for headline numbers. KevinPriceLoader fronts market_data + Alpaca.
This commit is contained in:
parent
7dcce5ea0e
commit
23ce45a4f2
6 changed files with 794 additions and 41 deletions
|
|
@ -57,8 +57,10 @@ class KevinStrategy:
|
|||
is_tradable: bool,
|
||||
) -> KevinDecision:
|
||||
symbol = mention.symbol
|
||||
action = mention.action
|
||||
horizon = mention.time_horizon
|
||||
# Normalize the action/horizon to their str value so the strategy works
|
||||
# with both SQLAlchemy enum instances and lightweight stubs (backtest).
|
||||
action_value = getattr(mention.action, "value", mention.action)
|
||||
horizon_value = getattr(mention.time_horizon, "value", mention.time_horizon)
|
||||
|
||||
# 1. Common no-trade gates
|
||||
if not is_tradable:
|
||||
|
|
@ -76,15 +78,15 @@ class KevinStrategy:
|
|||
)
|
||||
|
||||
# 2. Action-specific gates
|
||||
if action in (TickerAction.HOLD, TickerAction.WATCH):
|
||||
if action_value in (TickerAction.HOLD.value, TickerAction.WATCH.value):
|
||||
return KevinDecision(
|
||||
decision=KevinDecisionType.NO_OP,
|
||||
symbol=symbol,
|
||||
rationale=f"action={action.value} is UI-only, never trades",
|
||||
rationale=f"action={action_value} is UI-only, never trades",
|
||||
)
|
||||
|
||||
# 3. SELL — close long if held, else no-op
|
||||
if action == TickerAction.SELL:
|
||||
if action_value == TickerAction.SELL.value:
|
||||
if account.is_held(symbol):
|
||||
return KevinDecision(
|
||||
decision=KevinDecisionType.CLOSE_LONG,
|
||||
|
|
@ -99,7 +101,7 @@ class KevinStrategy:
|
|||
)
|
||||
|
||||
# 4. AVOID — close long if held + bridge will add blocklist (side effect)
|
||||
if action == TickerAction.AVOID:
|
||||
if action_value == TickerAction.AVOID.value:
|
||||
if account.is_held(symbol) and self.config.avoid_closes_longs:
|
||||
return KevinDecision(
|
||||
decision=KevinDecisionType.CLOSE_LONG,
|
||||
|
|
@ -120,7 +122,7 @@ class KevinStrategy:
|
|||
)
|
||||
|
||||
# 5. BUY path — full filter stack
|
||||
assert action == TickerAction.BUY
|
||||
assert action_value == TickerAction.BUY.value
|
||||
|
||||
if effective_conviction < self.config.min_conviction:
|
||||
return KevinDecision(
|
||||
|
|
@ -132,7 +134,7 @@ class KevinStrategy:
|
|||
),
|
||||
)
|
||||
|
||||
if horizon == TimeHorizon.INTRADAY:
|
||||
if horizon_value == TimeHorizon.INTRADAY.value:
|
||||
return KevinDecision(
|
||||
decision=KevinDecisionType.NO_OP,
|
||||
symbol=symbol,
|
||||
|
|
@ -197,7 +199,7 @@ class KevinStrategy:
|
|||
target_dollars = target_dollars.quantize(Decimal("0.01"))
|
||||
|
||||
holding_days = self.config.hold_days_by_horizon.get(
|
||||
horizon.value, self.config.hold_days_by_horizon["unspecified"]
|
||||
horizon_value, self.config.hold_days_by_horizon["unspecified"]
|
||||
)
|
||||
|
||||
return KevinDecision(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue