Exclude open paper trades from discovery
This commit is contained in:
@@ -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 = []
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user