feat(kevin-exec): size from target_dollars, propagate price, bracket entries

Kevin signals never placed orders: the executor sized only from
sentiment_context["current_price"] (None for Kevin) so qty=0, and orders
were always built SIMPLE (stop/take pcts ignored).

- TradeSignal gains `current_price`; the bridge now sets it on publish
- risk_manager honors `target_dollars` directly (no strength re-scale) and
  resolves price from current_price then sentiment_context
- executor builds BRACKET orders for LONG entries carrying stop/take pcts;
  EXIT/SELL signals stay SIMPLE (the bridge sets pcts even on exits)

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-04 21:56:59 +00:00
parent 7d1d4464c9
commit 14407d37dc
6 changed files with 302 additions and 26 deletions

View file

@ -56,6 +56,38 @@ async def _next_market_open(broker: AlpacaBroker) -> datetime:
return next_open.astimezone(timezone.utc)
def _build_order_request(
signal: TradeSignal,
side: OrderSide,
qty: float,
risk_manager: RiskManager,
) -> OrderRequest:
"""Build a BRACKET order for LONG entries that carry both stop/take
percentages; otherwise a SIMPLE order.
The bridge stamps stop/take pcts even on EXIT signals, so the
LONG-direction guard is what keeps exits SIMPLE.
"""
is_entry = signal.direction == SignalDirection.LONG
if (
is_entry
and signal.stop_loss_pct is not None
and signal.take_profit_pct is not None
):
entry = risk_manager._resolve_price(signal)
stop_loss_price = round(entry * (1 - float(signal.stop_loss_pct)), 2)
take_profit_price = round(entry * (1 + float(signal.take_profit_pct)), 2)
return OrderRequest(
ticker=signal.ticker,
side=side,
qty=float(qty),
order_class="bracket",
take_profit_price=take_profit_price,
stop_loss_price=stop_loss_price,
)
return OrderRequest(ticker=signal.ticker, side=side, qty=float(qty))
async def process_signal(
signal: TradeSignal,
risk_manager: RiskManager,
@ -133,11 +165,7 @@ async def process_signal(
# --- Step 3: create order ---
side = OrderSide.BUY if signal.direction == SignalDirection.LONG else OrderSide.SELL
order_request = OrderRequest(
ticker=signal.ticker,
side=side,
qty=float(qty),
)
order_request = _build_order_request(signal, side, qty, risk_manager)
# --- Step 4: submit order ---
start = time.monotonic()