feat: add pre-market planner config and remove static watchlists (issue #78) #98
@@ -55,8 +55,15 @@ class Settings(BaseSettings):
|
|||||||
DAILY_SESSIONS: int = Field(default=4, ge=1, le=10)
|
DAILY_SESSIONS: int = Field(default=4, ge=1, le=10)
|
||||||
SESSION_INTERVAL_HOURS: int = Field(default=6, ge=1, le=24)
|
SESSION_INTERVAL_HOURS: int = Field(default=6, ge=1, le=24)
|
||||||
|
|
||||||
|
# Pre-Market Planner
|
||||||
|
PRE_MARKET_MINUTES: int = Field(default=30, ge=10, le=120)
|
||||||
|
MAX_SCENARIOS_PER_STOCK: int = Field(default=5, ge=1, le=10)
|
||||||
|
PLANNER_TIMEOUT_SECONDS: int = Field(default=60, ge=10, le=300)
|
||||||
|
DEFENSIVE_PLAYBOOK_ON_FAILURE: bool = True
|
||||||
|
RESCAN_INTERVAL_SECONDS: int = Field(default=300, ge=60, le=900)
|
||||||
|
|
||||||
# Market selection (comma-separated market codes)
|
# Market selection (comma-separated market codes)
|
||||||
ENABLED_MARKETS: str = "KR"
|
ENABLED_MARKETS: str = "KR,US"
|
||||||
|
|
||||||
# Backup and Disaster Recovery (optional)
|
# Backup and Disaster Recovery (optional)
|
||||||
BACKUP_ENABLED: bool = True
|
BACKUP_ENABLED: bool = True
|
||||||
|
|||||||
79
src/main.py
79
src/main.py
@@ -63,14 +63,6 @@ def safe_float(value: str | float | None, default: float = 0.0) -> float:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
# Target stock codes to monitor per market
|
|
||||||
WATCHLISTS = {
|
|
||||||
"KR": ["005930", "000660", "035420"], # Samsung, SK Hynix, NAVER
|
|
||||||
"US_NASDAQ": ["AAPL", "MSFT", "GOOGL"], # Example US stocks
|
|
||||||
"US_NYSE": ["JPM", "BAC"], # Example NYSE stocks
|
|
||||||
"JP": ["7203", "6758"], # Toyota, Sony
|
|
||||||
}
|
|
||||||
|
|
||||||
TRADE_INTERVAL_SECONDS = 60
|
TRADE_INTERVAL_SECONDS = 60
|
||||||
SCAN_INTERVAL_SECONDS = 60 # Scan markets every 60 seconds
|
SCAN_INTERVAL_SECONDS = 60 # Scan markets every 60 seconds
|
||||||
MAX_CONNECTION_RETRIES = 3
|
MAX_CONNECTION_RETRIES = 3
|
||||||
@@ -79,15 +71,6 @@ MAX_CONNECTION_RETRIES = 3
|
|||||||
DAILY_TRADE_SESSIONS = 4 # Number of trading sessions per day
|
DAILY_TRADE_SESSIONS = 4 # Number of trading sessions per day
|
||||||
TRADE_SESSION_INTERVAL_HOURS = 6 # Hours between sessions
|
TRADE_SESSION_INTERVAL_HOURS = 6 # Hours between sessions
|
||||||
|
|
||||||
# Full stock universe per market (for scanning)
|
|
||||||
# In production, this would be loaded from a database or API
|
|
||||||
STOCK_UNIVERSE = {
|
|
||||||
"KR": ["005930", "000660", "035420", "051910", "005380", "005490"],
|
|
||||||
"US_NASDAQ": ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "TSLA"],
|
|
||||||
"US_NYSE": ["JPM", "BAC", "XOM", "JNJ", "V"],
|
|
||||||
"JP": ["7203", "6758", "9984", "6861"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def trading_cycle(
|
async def trading_cycle(
|
||||||
broker: KISBroker,
|
broker: KISBroker,
|
||||||
@@ -349,6 +332,7 @@ async def run_daily_session(
|
|||||||
criticality_assessor: CriticalityAssessor,
|
criticality_assessor: CriticalityAssessor,
|
||||||
telegram: TelegramClient,
|
telegram: TelegramClient,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
smart_scanner: SmartVolatilityScanner | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Execute one daily trading session.
|
"""Execute one daily trading session.
|
||||||
|
|
||||||
@@ -368,15 +352,21 @@ async def run_daily_session(
|
|||||||
|
|
||||||
# Process each open market
|
# Process each open market
|
||||||
for market in open_markets:
|
for market in open_markets:
|
||||||
# Get watchlist for this market
|
# Dynamic stock discovery via scanner (no static watchlists)
|
||||||
watchlist = WATCHLISTS.get(market.code, [])
|
try:
|
||||||
|
candidates = await smart_scanner.scan()
|
||||||
|
watchlist = [c.stock_code for c in candidates] if candidates else []
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Smart Scanner failed for %s: %s", market.name, exc)
|
||||||
|
watchlist = []
|
||||||
|
|
||||||
if not watchlist:
|
if not watchlist:
|
||||||
logger.debug("No watchlist for market %s", market.code)
|
logger.info("No scanner candidates for market %s — skipping", market.code)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info("Processing market: %s (%d stocks)", market.name, len(watchlist))
|
logger.info("Processing market: %s (%d stocks)", market.name, len(watchlist))
|
||||||
|
|
||||||
# Collect market data for all stocks in the watchlist
|
# Collect market data for all stocks from scanner
|
||||||
stocks_data = []
|
stocks_data = []
|
||||||
for stock_code in watchlist:
|
for stock_code in watchlist:
|
||||||
try:
|
try:
|
||||||
@@ -745,6 +735,9 @@ async def run(settings: Settings) -> None:
|
|||||||
# Track scan candidates for selection context logging
|
# Track scan candidates for selection context logging
|
||||||
scan_candidates: dict[str, ScanCandidate] = {} # stock_code -> candidate
|
scan_candidates: dict[str, ScanCandidate] = {} # stock_code -> candidate
|
||||||
|
|
||||||
|
# Active stocks per market (dynamically discovered by scanner)
|
||||||
|
active_stocks: dict[str, list[str]] = {} # market_code -> [stock_codes]
|
||||||
|
|
||||||
# Initialize latency control system
|
# Initialize latency control system
|
||||||
criticality_assessor = CriticalityAssessor(
|
criticality_assessor = CriticalityAssessor(
|
||||||
critical_pnl_threshold=-2.5, # Near circuit breaker at -3.0%
|
critical_pnl_threshold=-2.5, # Near circuit breaker at -3.0%
|
||||||
@@ -817,6 +810,7 @@ async def run(settings: Settings) -> None:
|
|||||||
criticality_assessor,
|
criticality_assessor,
|
||||||
telegram,
|
telegram,
|
||||||
settings,
|
settings,
|
||||||
|
smart_scanner=smart_scanner,
|
||||||
)
|
)
|
||||||
except CircuitBreakerTripped:
|
except CircuitBreakerTripped:
|
||||||
logger.critical("Circuit breaker tripped — shutting down")
|
logger.critical("Circuit breaker tripped — shutting down")
|
||||||
@@ -890,57 +884,52 @@ async def run(settings: Settings) -> None:
|
|||||||
logger.warning("Market open notification failed: %s", exc)
|
logger.warning("Market open notification failed: %s", exc)
|
||||||
_market_states[market.code] = True
|
_market_states[market.code] = True
|
||||||
|
|
||||||
# Smart Scanner: Python-first filtering (RSI + volume) before AI
|
# Smart Scanner: dynamic stock discovery (no static watchlists)
|
||||||
now_timestamp = asyncio.get_event_loop().time()
|
now_timestamp = asyncio.get_event_loop().time()
|
||||||
last_scan = last_scan_time.get(market.code, 0.0)
|
last_scan = last_scan_time.get(market.code, 0.0)
|
||||||
if now_timestamp - last_scan >= SCAN_INTERVAL_SECONDS:
|
if now_timestamp - last_scan >= SCAN_INTERVAL_SECONDS:
|
||||||
try:
|
try:
|
||||||
logger.info("Smart Scanner: Scanning %s market", market.name)
|
logger.info("Smart Scanner: Scanning %s market", market.name)
|
||||||
|
|
||||||
# Run smart scan with fallback to static universe
|
candidates = await smart_scanner.scan()
|
||||||
fallback_universe = STOCK_UNIVERSE.get(market.code, [])
|
|
||||||
candidates = await smart_scanner.scan(fallback_stocks=fallback_universe)
|
|
||||||
|
|
||||||
if candidates:
|
if candidates:
|
||||||
# Update watchlist with qualified candidates
|
# Use scanner results directly as trading candidates
|
||||||
qualified_codes = smart_scanner.get_stock_codes(candidates)
|
active_stocks[market.code] = smart_scanner.get_stock_codes(
|
||||||
|
candidates
|
||||||
|
)
|
||||||
|
|
||||||
# Merge with existing watchlist (keep some continuity)
|
# Store candidates for selection context logging
|
||||||
current_watchlist = WATCHLISTS.get(market.code, [])
|
|
||||||
# Keep up to 2 from existing, add new qualified
|
|
||||||
merged = qualified_codes + [
|
|
||||||
c for c in current_watchlist if c not in qualified_codes
|
|
||||||
][:2]
|
|
||||||
WATCHLISTS[market.code] = merged[:5] # Cap at 5
|
|
||||||
|
|
||||||
# Store candidates for later selection context logging
|
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
scan_candidates[candidate.stock_code] = candidate
|
scan_candidates[candidate.stock_code] = candidate
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Smart Scanner: Found %d qualified candidates for %s: %s",
|
"Smart Scanner: Found %d candidates for %s: %s",
|
||||||
len(candidates),
|
len(candidates),
|
||||||
market.name,
|
market.name,
|
||||||
[f"{c.stock_code}(RSI={c.rsi:.0f})" for c in candidates],
|
[f"{c.stock_code}(RSI={c.rsi:.0f})" for c in candidates],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Smart Scanner: No qualified candidates for %s", market.name)
|
logger.info(
|
||||||
|
"Smart Scanner: No candidates for %s — no trades", market.name
|
||||||
|
)
|
||||||
|
active_stocks[market.code] = []
|
||||||
|
|
||||||
last_scan_time[market.code] = now_timestamp
|
last_scan_time[market.code] = now_timestamp
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Smart Scanner failed for %s: %s", market.name, exc)
|
logger.error("Smart Scanner failed for %s: %s", market.name, exc)
|
||||||
|
|
||||||
# Get watchlist for this market
|
# Get active stocks from scanner (dynamic, no static fallback)
|
||||||
watchlist = WATCHLISTS.get(market.code, [])
|
stock_codes = active_stocks.get(market.code, [])
|
||||||
if not watchlist:
|
if not stock_codes:
|
||||||
logger.debug("No watchlist for market %s", market.code)
|
logger.debug("No active stocks for market %s", market.code)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info("Processing market: %s (%d stocks)", market.name, len(watchlist))
|
logger.info("Processing market: %s (%d stocks)", market.name, len(stock_codes))
|
||||||
|
|
||||||
# Process each stock in the watchlist
|
# Process each stock from scanner results
|
||||||
for stock_code in watchlist:
|
for stock_code in stock_codes:
|
||||||
if shutdown.is_set():
|
if shutdown.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user