fix: 6 buy/sell logic bugs — sells check trading window, persist audit trail, dedup after position check, no duplicate buys, fix stop-level insert, profit-taking respects market hours

This commit is contained in:
Celes Renata
2026-04-17 00:07:50 +00:00
parent 1246b3868b
commit 29f46d387c
+62 -18
View File
@@ -686,8 +686,6 @@ class TradingEngine:
already = await self.redis.get(dedupe_key) already = await self.redis.get(dedupe_key)
if already: if already:
continue continue
# Set dedupe key with 24h TTL before evaluation
await self.redis.set(dedupe_key, "1", ex=86400)
# Ensure portfolio state exists # Ensure portfolio state exists
if self.portfolio_state is None: if self.portfolio_state is None:
@@ -697,6 +695,11 @@ class TradingEngine:
# --- Sell path: skip position sizing, look up existing position --- # --- Sell path: skip position sizing, look up existing position ---
if action == "sell": if action == "sell":
# Check trading window for sells too
now_check = datetime.now(tz=timezone.utc)
if not is_within_trading_window(now_check):
continue
pos_row = None pos_row = None
try: try:
pos_row = await self.pool.fetchrow( pos_row = await self.pool.fetchrow(
@@ -708,9 +711,12 @@ class TradingEngine:
logger.debug("Could not look up position for sell: %s", ticker) logger.debug("Could not look up position for sell: %s", ticker)
if pos_row is None: if pos_row is None:
logger.info("Sell recommendation for %s but no open position — skipping", ticker)
continue continue
# Set dedup key only after confirming we have a position to sell
if self.redis is not None:
await self.redis.set(trading_dedupe_key(rec_id), "1", ex=86400)
sell_qty = int(pos_row["quantity"]) sell_qty = int(pos_row["quantity"])
sell_price = rec.get("current_price", 0.0) sell_price = rec.get("current_price", 0.0)
estimated_proceeds = sell_qty * sell_price estimated_proceeds = sell_qty * sell_price
@@ -719,6 +725,7 @@ class TradingEngine:
"trading_decision_id": str(uuid.uuid4()), "trading_decision_id": str(uuid.uuid4()),
"ticker": ticker, "ticker": ticker,
"action": "sell", "action": "sell",
"side": "sell",
"quantity": sell_qty, "quantity": sell_qty,
"order_type": "market", "order_type": "market",
"source": "trading_engine", "source": "trading_engine",
@@ -738,13 +745,54 @@ class TradingEngine:
) )
self.portfolio_state.active_pool += estimated_proceeds self.portfolio_state.active_pool += estimated_proceeds
# Mark as processed # Persist sell decision for audit trail
sell_decision = TradingDecision(
id=order_job["trading_decision_id"],
recommendation_id=rec_id,
decision="act",
skip_reason=None,
ticker=ticker,
computed_position_size=estimated_proceeds,
computed_share_quantity=sell_qty,
risk_tier_at_decision=self._active_risk_tier.name,
portfolio_heat_at_decision=self.portfolio_state.portfolio_heat if self.portfolio_state else 0,
active_pool_at_decision=self.portfolio_state.active_pool if self.portfolio_state else 0,
reserve_pool_at_decision=self.portfolio_state.reserve_pool if self.portfolio_state else 0,
circuit_breaker_status="active" if self._cb_state.active else "inactive",
decision_trace={"action": "sell", "reasoning": [f"Sell {sell_qty} shares of {ticker}"]},
)
await self._persist_decision(sell_decision)
# Deactivate stop levels for sold position
try:
await self.pool.execute(
"UPDATE position_stop_levels SET active = FALSE, updated_at = NOW() WHERE ticker = $1 AND active = TRUE",
ticker,
)
except Exception:
pass
if rec_id: if rec_id:
self.processed_recommendation_ids.add(rec_id) self.processed_recommendation_ids.add(rec_id)
continue continue
# --- Buy path: evaluate recommendation through position sizer --- # --- Buy path ---
# Evaluate recommendation # Set dedup key for buys
if self.redis is not None:
await self.redis.set(trading_dedupe_key(rec_id), "1", ex=86400)
# Check if we already hold this ticker — don't double up
try:
existing_pos = await self.pool.fetchrow(
"SELECT quantity FROM positions WHERE ticker = $1 AND quantity > 0",
ticker,
)
if existing_pos:
continue
except Exception:
pass
# Evaluate recommendation through position sizer
decision = self.evaluate_recommendation( decision = self.evaluate_recommendation(
rec=rec, rec=rec,
portfolio_state=self.portfolio_state, portfolio_state=self.portfolio_state,
@@ -803,12 +851,7 @@ class TradingEngine:
"(ticker, entry_price, stop_loss_price, take_profit_price, " "(ticker, entry_price, stop_loss_price, take_profit_price, "
"trailing_stop_active, atr_value, atr_multiplier, " "trailing_stop_active, atr_value, atr_multiplier, "
"reward_risk_ratio, signal_confidence, is_micro_trade, active) " "reward_risk_ratio, signal_confidence, is_micro_trade, active) "
"VALUES ($1, $2, $3, $4, FALSE, $5, $6, $7, $8, FALSE, TRUE) " "VALUES ($1, $2, $3, $4, FALSE, $5, $6, $7, $8, FALSE, TRUE)",
"ON CONFLICT (ticker) WHERE active = TRUE DO UPDATE SET "
"entry_price = EXCLUDED.entry_price, "
"stop_loss_price = EXCLUDED.stop_loss_price, "
"take_profit_price = EXCLUDED.take_profit_price, "
"updated_at = NOW()",
decision.ticker, price, sl_price, tp_price, decision.ticker, price, sl_price, tp_price,
atr_est, tier.stop_loss_atr_multiplier, atr_est, tier.stop_loss_atr_multiplier,
tier.reward_risk_ratio, tier.reward_risk_ratio,
@@ -1720,16 +1763,17 @@ class TradingEngine:
async def _check_profit_taking(self) -> None: async def _check_profit_taking(self) -> None:
"""Check positions for profit-taking opportunities. """Check positions for profit-taking opportunities.
Sells positions that have gained more than the take-profit threshold Sells positions that have gained more than the take-profit threshold.
defined by the risk tier's reward_risk_ratio. For moderate tier with Only runs during trading window hours.
2.0 ATR stop and 1.5 reward/risk, that's roughly a 3% gain target.
Also sells positions that have gained > 10% regardless of tier
(absolute profit cap to lock in gains).
""" """
if self.pool is None: if self.pool is None:
return return
# Only sell during market hours
now = datetime.now(tz=timezone.utc)
if not is_within_trading_window(now):
return
try: try:
rows = await self.pool.fetch( rows = await self.pool.fetch(
"SELECT ticker, quantity, avg_entry_price, current_price, unrealized_pnl " "SELECT ticker, quantity, avg_entry_price, current_price, unrealized_pnl "