Compare commits

...

2 Commits

Author SHA1 Message Date
agentson
1a34a74232 feat: add pre-market planner config and remove static watchlists (issue #78)
Some checks failed
CI / test (pull_request) Has been cancelled
- Add pre-market planner settings: PRE_MARKET_MINUTES, MAX_SCENARIOS_PER_STOCK,
  PLANNER_TIMEOUT_SECONDS, DEFENSIVE_PLAYBOOK_ON_FAILURE, RESCAN_INTERVAL_SECONDS
- Change ENABLED_MARKETS default from KR to KR,US
- Remove static WATCHLISTS and STOCK_UNIVERSE dictionaries from main.py
- Replace watchlist-based trading with dynamic scanner-only stock discovery
- SmartVolatilityScanner is now the sole source of trading candidates
- Add active_stocks dict for scanner-discovered stocks per market
- Add smart_scanner parameter to run_daily_session()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 01:58:09 +09:00
a82a167915 Merge pull request 'feat: implement Smart Volatility Scanner (issue #76)' (#77) from feature/issue-76-smart-volatility-scanner into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #77
2026-02-06 07:43:54 +09:00
2 changed files with 42 additions and 46 deletions

View File

@@ -55,8 +55,15 @@ class Settings(BaseSettings):
DAILY_SESSIONS: int = Field(default=4, ge=1, le=10)
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)
ENABLED_MARKETS: str = "KR"
ENABLED_MARKETS: str = "KR,US"
# Backup and Disaster Recovery (optional)
BACKUP_ENABLED: bool = True

View File

@@ -63,14 +63,6 @@ def safe_float(value: str | float | None, default: float = 0.0) -> float:
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
SCAN_INTERVAL_SECONDS = 60 # Scan markets every 60 seconds
MAX_CONNECTION_RETRIES = 3
@@ -79,15 +71,6 @@ MAX_CONNECTION_RETRIES = 3
DAILY_TRADE_SESSIONS = 4 # Number of trading sessions per day
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(
broker: KISBroker,
@@ -349,6 +332,7 @@ async def run_daily_session(
criticality_assessor: CriticalityAssessor,
telegram: TelegramClient,
settings: Settings,
smart_scanner: SmartVolatilityScanner | None = None,
) -> None:
"""Execute one daily trading session.
@@ -368,15 +352,21 @@ async def run_daily_session(
# Process each open market
for market in open_markets:
# Get watchlist for this market
watchlist = WATCHLISTS.get(market.code, [])
# Dynamic stock discovery via scanner (no static watchlists)
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:
logger.debug("No watchlist for market %s", market.code)
logger.info("No scanner candidates for market %s — skipping", market.code)
continue
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 = []
for stock_code in watchlist:
try:
@@ -745,6 +735,9 @@ async def run(settings: Settings) -> None:
# Track scan candidates for selection context logging
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
criticality_assessor = CriticalityAssessor(
critical_pnl_threshold=-2.5, # Near circuit breaker at -3.0%
@@ -817,6 +810,7 @@ async def run(settings: Settings) -> None:
criticality_assessor,
telegram,
settings,
smart_scanner=smart_scanner,
)
except CircuitBreakerTripped:
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)
_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()
last_scan = last_scan_time.get(market.code, 0.0)
if now_timestamp - last_scan >= SCAN_INTERVAL_SECONDS:
try:
logger.info("Smart Scanner: Scanning %s market", market.name)
# Run smart scan with fallback to static universe
fallback_universe = STOCK_UNIVERSE.get(market.code, [])
candidates = await smart_scanner.scan(fallback_stocks=fallback_universe)
candidates = await smart_scanner.scan()
if candidates:
# Update watchlist with qualified candidates
qualified_codes = smart_scanner.get_stock_codes(candidates)
# Use scanner results directly as trading candidates
active_stocks[market.code] = smart_scanner.get_stock_codes(
candidates
)
# Merge with existing watchlist (keep some continuity)
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
# Store candidates for selection context logging
for candidate in candidates:
scan_candidates[candidate.stock_code] = candidate
logger.info(
"Smart Scanner: Found %d qualified candidates for %s: %s",
"Smart Scanner: Found %d candidates for %s: %s",
len(candidates),
market.name,
[f"{c.stock_code}(RSI={c.rsi:.0f})" for c in candidates],
)
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
except Exception as exc:
logger.error("Smart Scanner failed for %s: %s", market.name, exc)
# Get watchlist for this market
watchlist = WATCHLISTS.get(market.code, [])
if not watchlist:
logger.debug("No watchlist for market %s", market.code)
# Get active stocks from scanner (dynamic, no static fallback)
stock_codes = active_stocks.get(market.code, [])
if not stock_codes:
logger.debug("No active stocks for market %s", market.code)
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
for stock_code in watchlist:
# Process each stock from scanner results
for stock_code in stock_codes:
if shutdown.is_set():
break