Exclude open paper trades from discovery
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m25s
Deploy / deploy (push) Successful in 43s

This commit is contained in:
2026-07-04 13:01:58 +02:00
parent edc1a9757b
commit 23d1db1f30
4 changed files with 113 additions and 1 deletions
+1
View File
@@ -35,6 +35,7 @@ async def list_trade_setups(
min_confidence=min_confidence, min_confidence=min_confidence,
recommended_action=recommended_action, recommended_action=recommended_action,
live_recommendation=True, live_recommendation=True,
exclude_open_trade_tickers=True,
) )
data = [] data = []
+5 -1
View File
@@ -286,7 +286,11 @@ async def _watchlist_tickers(db: AsyncSession) -> list[tuple[int, str]]:
async def _qualified_setups(db: AsyncSession) -> list[dict]: async def _qualified_setups(db: AsyncSession) -> list[dict]:
# live_recommendation: gate and format on current score/sentiment context, # live_recommendation: gate and format on current score/sentiment context,
# not the values frozen into the setup at scan time. # not the values frozen into the setup at scan time.
setups = await get_trade_setups(db, live_recommendation=True) setups = await get_trade_setups(
db,
live_recommendation=True,
exclude_open_trade_tickers=True,
)
config = await get_activation_config(db) config = await get_activation_config(db)
return [s for s in setups if setup_qualifies(SimpleNamespace(**s), config)] return [s for s in setups if setup_qualifies(SimpleNamespace(**s), config)]
+11
View File
@@ -19,6 +19,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.exceptions import NotFoundError from app.exceptions import NotFoundError
from app.models.fundamental import FundamentalData from app.models.fundamental import FundamentalData
from app.models.ohlcv import OHLCVRecord from app.models.ohlcv import OHLCVRecord
from app.models.paper_trade import PaperTrade
from app.models.score import CompositeScore, DimensionScore from app.models.score import CompositeScore, DimensionScore
from app.models.sentiment import SentimentScore from app.models.sentiment import SentimentScore
from app.models.signal_context_snapshot import SignalContextSnapshot from app.models.signal_context_snapshot import SignalContextSnapshot
@@ -599,6 +600,7 @@ async def get_trade_setups(
recommended_action: str | None = None, recommended_action: str | None = None,
symbol: str | None = None, symbol: str | None = None,
live_recommendation: bool = False, live_recommendation: bool = False,
exclude_open_trade_tickers: bool = False,
) -> list[dict]: ) -> list[dict]:
"""Get latest stored trade setups, optionally filtered.""" """Get latest stored trade setups, optionally filtered."""
stmt = ( stmt = (
@@ -615,6 +617,15 @@ async def get_trade_setups(
stmt = stmt.where(TradeSetup.confidence_score >= min_confidence) stmt = stmt.where(TradeSetup.confidence_score >= min_confidence)
if recommended_action is not None and not live_recommendation: if recommended_action is not None and not live_recommendation:
stmt = stmt.where(TradeSetup.recommended_action == recommended_action) stmt = stmt.where(TradeSetup.recommended_action == recommended_action)
if exclude_open_trade_tickers:
open_trade_result = await db.execute(
select(PaperTrade.ticker_id)
.where(PaperTrade.status == "open")
.distinct()
)
open_ticker_ids = {ticker_id for ticker_id, in open_trade_result.all()}
if open_ticker_ids:
stmt = stmt.where(~TradeSetup.ticker_id.in_(open_ticker_ids))
stmt = stmt.order_by(TradeSetup.detected_at.desc(), TradeSetup.id.desc()) stmt = stmt.order_by(TradeSetup.detected_at.desc(), TradeSetup.id.desc())
@@ -21,12 +21,14 @@ from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.models.ohlcv import OHLCVRecord from app.models.ohlcv import OHLCVRecord
from app.models.paper_trade import PaperTrade
from app.models.signal_context_snapshot import SignalContextSnapshot from app.models.signal_context_snapshot import SignalContextSnapshot
from app.models.sr_level import SRLevel from app.models.sr_level import SRLevel
from app.models.ticker import Ticker from app.models.ticker import Ticker
from app.models.trade_setup import TradeSetup from app.models.trade_setup import TradeSetup
from app.models.score import CompositeScore, DimensionScore from app.models.score import CompositeScore, DimensionScore
from app.models.sentiment import SentimentScore from app.models.sentiment import SentimentScore
from app.models.user import User
from app.services.rr_scanner_service import scan_ticker, get_trade_setups from app.services.rr_scanner_service import scan_ticker, get_trade_setups
@@ -442,6 +444,100 @@ async def test_get_trade_setups_sorting_rr_desc_composite_desc(db_session: Async
) )
@pytest.mark.asyncio
async def test_get_trade_setups_can_exclude_tickers_with_open_paper_trades(
db_session: AsyncSession,
):
now = datetime.now(timezone.utc)
if await db_session.get(User, 1) is None:
db_session.add(
User(id=1, username="u", password_hash="x", role="user", has_access=True)
)
await db_session.flush()
open_ticker = Ticker(symbol="OPENQ")
closed_ticker = Ticker(symbol="CLOSEDQ")
free_ticker = Ticker(symbol="FREEQ")
db_session.add_all([open_ticker, closed_ticker, free_ticker])
await db_session.flush()
db_session.add_all([
TradeSetup(
ticker_id=open_ticker.id,
direction="long",
entry_price=100.0,
stop_loss=95.0,
target=112.0,
rr_ratio=2.4,
composite_score=80.0,
detected_at=now,
),
TradeSetup(
ticker_id=closed_ticker.id,
direction="long",
entry_price=100.0,
stop_loss=95.0,
target=113.0,
rr_ratio=2.6,
composite_score=70.0,
detected_at=now,
),
TradeSetup(
ticker_id=free_ticker.id,
direction="long",
entry_price=100.0,
stop_loss=95.0,
target=114.0,
rr_ratio=2.8,
composite_score=60.0,
detected_at=now,
),
PaperTrade(
user_id=1,
ticker_id=open_ticker.id,
direction="long",
entry_price=100.0,
shares=10.0,
stop_loss=95.0,
target=112.0,
status="open",
opened_at=now,
),
PaperTrade(
user_id=1,
ticker_id=closed_ticker.id,
direction="long",
entry_price=100.0,
shares=10.0,
stop_loss=95.0,
target=113.0,
status="closed",
opened_at=now - timedelta(days=2),
close_price=113.0,
closed_at=now - timedelta(days=1),
close_reason="target",
),
])
await db_session.flush()
default_rows = await get_trade_setups(db_session)
assert {"OPENQ", "CLOSEDQ", "FREEQ"}.issubset(
{row["symbol"] for row in default_rows}
)
discovery_rows = await get_trade_setups(
db_session,
exclude_open_trade_tickers=True,
)
discovery_symbols = {row["symbol"] for row in discovery_rows}
assert "OPENQ" not in discovery_symbols
assert {"CLOSEDQ", "FREEQ"}.issubset(discovery_symbols)
ticker_rows = await get_trade_setups(db_session, symbol="OPENQ")
assert [row["symbol"] for row in ticker_rows] == ["OPENQ"]
async def _seed_stale_setup_with_current_scores(db_session: AsyncSession) -> TradeSetup: async def _seed_stale_setup_with_current_scores(db_session: AsyncSession) -> TradeSetup:
"""Stored setup frozen at scan time (conf 82, neutral) vs. current context """Stored setup frozen at scan time (conf 82, neutral) vs. current context
(bullish sentiment, composite 96) that yields live confidence 97.""" (bullish sentiment, composite 96) that yields live confidence 97."""