Compare commits
1 Commits
42db5b3cc1
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d19e5b0de6 |
22
src/db.py
22
src/db.py
@@ -237,6 +237,28 @@ def get_open_position(
|
||||
return {"decision_id": row[1], "price": row[2], "quantity": row[3]}
|
||||
|
||||
|
||||
def get_open_positions_by_market(
|
||||
conn: sqlite3.Connection, market: str
|
||||
) -> list[str]:
|
||||
"""Return stock codes with a net positive position in the given market.
|
||||
|
||||
Uses net BUY - SELL quantity aggregation to avoid false positives from
|
||||
the simpler "latest record is BUY" heuristic. A stock is considered
|
||||
open only when the bot's own recorded trades leave a positive net quantity.
|
||||
"""
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT stock_code
|
||||
FROM trades
|
||||
WHERE market = ?
|
||||
GROUP BY stock_code
|
||||
HAVING SUM(CASE WHEN action = 'BUY' THEN quantity ELSE -quantity END) > 0
|
||||
""",
|
||||
(market,),
|
||||
)
|
||||
return [row[0] for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def get_recent_symbols(
|
||||
conn: sqlite3.Connection, market: str, limit: int = 30
|
||||
) -> list[str]:
|
||||
|
||||
17
src/main.py
17
src/main.py
@@ -32,6 +32,7 @@ from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected, Risk
|
||||
from src.db import (
|
||||
get_latest_buy_trade,
|
||||
get_open_position,
|
||||
get_open_positions_by_market,
|
||||
get_recent_symbols,
|
||||
init_db,
|
||||
log_trade,
|
||||
@@ -1864,7 +1865,21 @@ async def run(settings: Settings) -> None:
|
||||
logger.error("Smart Scanner failed for %s: %s", market.name, exc)
|
||||
|
||||
# Get active stocks from scanner (dynamic, no static fallback)
|
||||
stock_codes = active_stocks.get(market.code, [])
|
||||
# Also include current holdings so stop-loss / take-profit
|
||||
# can trigger even when a position drops off the scanner.
|
||||
scanner_codes = active_stocks.get(market.code, [])
|
||||
held_codes = get_open_positions_by_market(db_conn, market.code)
|
||||
# Union: scanner candidates first, then holdings not already present.
|
||||
# dict.fromkeys preserves insertion order and removes duplicates.
|
||||
stock_codes = list(dict.fromkeys(scanner_codes + held_codes))
|
||||
if held_codes:
|
||||
new_held = [c for c in held_codes if c not in set(scanner_codes)]
|
||||
if new_held:
|
||||
logger.info(
|
||||
"Holdings added to loop for %s (not in scanner): %s",
|
||||
market.name,
|
||||
new_held,
|
||||
)
|
||||
if not stock_codes:
|
||||
logger.debug("No active stocks for market %s", market.code)
|
||||
continue
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for database helper functions."""
|
||||
|
||||
from src.db import get_open_position, init_db, log_trade
|
||||
from src.db import get_open_position, get_open_positions_by_market, init_db, log_trade
|
||||
|
||||
|
||||
def test_get_open_position_returns_latest_buy() -> None:
|
||||
@@ -58,3 +58,87 @@ def test_get_open_position_returns_none_when_latest_is_sell() -> None:
|
||||
def test_get_open_position_returns_none_when_no_trades() -> None:
|
||||
conn = init_db(":memory:")
|
||||
assert get_open_position(conn, "AAPL", "US_NASDAQ") is None
|
||||
|
||||
|
||||
# --- get_open_positions_by_market tests ---
|
||||
|
||||
|
||||
def test_get_open_positions_by_market_returns_net_positive_stocks() -> None:
|
||||
"""Stocks with net BUY quantity > 0 are included."""
|
||||
conn = init_db(":memory:")
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="BUY", confidence=90,
|
||||
rationale="entry", quantity=5, price=70000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d1",
|
||||
)
|
||||
log_trade(
|
||||
conn=conn, stock_code="000660", action="BUY", confidence=85,
|
||||
rationale="entry", quantity=3, price=100000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d2",
|
||||
)
|
||||
|
||||
result = get_open_positions_by_market(conn, "KR")
|
||||
assert set(result) == {"005930", "000660"}
|
||||
|
||||
|
||||
def test_get_open_positions_by_market_excludes_fully_sold_stocks() -> None:
|
||||
"""Stocks where BUY qty == SELL qty are excluded (net qty = 0)."""
|
||||
conn = init_db(":memory:")
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="BUY", confidence=90,
|
||||
rationale="entry", quantity=3, price=70000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d1",
|
||||
)
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="SELL", confidence=95,
|
||||
rationale="exit", quantity=3, price=71000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d2",
|
||||
)
|
||||
|
||||
result = get_open_positions_by_market(conn, "KR")
|
||||
assert "005930" not in result
|
||||
|
||||
|
||||
def test_get_open_positions_by_market_includes_partially_sold_stocks() -> None:
|
||||
"""Stocks with partial SELL (net qty > 0) are still included."""
|
||||
conn = init_db(":memory:")
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="BUY", confidence=90,
|
||||
rationale="entry", quantity=5, price=70000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d1",
|
||||
)
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="SELL", confidence=95,
|
||||
rationale="partial exit", quantity=2, price=71000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d2",
|
||||
)
|
||||
|
||||
result = get_open_positions_by_market(conn, "KR")
|
||||
assert "005930" in result
|
||||
|
||||
|
||||
def test_get_open_positions_by_market_is_market_scoped() -> None:
|
||||
"""Only stocks from the specified market are returned."""
|
||||
conn = init_db(":memory:")
|
||||
log_trade(
|
||||
conn=conn, stock_code="005930", action="BUY", confidence=90,
|
||||
rationale="entry", quantity=3, price=70000.0, market="KR",
|
||||
exchange_code="KRX", decision_id="d1",
|
||||
)
|
||||
log_trade(
|
||||
conn=conn, stock_code="AAPL", action="BUY", confidence=85,
|
||||
rationale="entry", quantity=2, price=200.0, market="NASD",
|
||||
exchange_code="NAS", decision_id="d2",
|
||||
)
|
||||
|
||||
kr_result = get_open_positions_by_market(conn, "KR")
|
||||
nasd_result = get_open_positions_by_market(conn, "NASD")
|
||||
|
||||
assert kr_result == ["005930"]
|
||||
assert nasd_result == ["AAPL"]
|
||||
|
||||
|
||||
def test_get_open_positions_by_market_returns_empty_when_no_trades() -> None:
|
||||
"""Empty list returned when no trades exist for the market."""
|
||||
conn = init_db(":memory:")
|
||||
assert get_open_positions_by_market(conn, "KR") == []
|
||||
|
||||
Reference in New Issue
Block a user