Files
KisStock/backend/app/workers/scheduler.py

176 lines
7.1 KiB
Python

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.core.market_schedule import market_schedule
from app.services.kis_client import kis_client
from app.services.realtime_manager import realtime_manager
from app.db.database import SessionLocal
from app.db.models import StockItem, ReservedOrder
from sqlalchemy import select, update
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
scheduler = AsyncIOScheduler()
async def market_check_job():
"""
Periodic check to ensure Realtime Manager is connected when market is open.
"""
logger.info("Scheduler: Market Check Job Running...")
is_domestic_open = market_schedule.is_market_open("Domestic")
# is_overseas_open = market_schedule.is_market_open("Overseas")
# If market is open and WS is not running, start it
if is_domestic_open and not realtime_manager.running:
logger.info("Market is Open! Starting Realtime Manager.")
else:
logger.info(f"Market Status: Domestic={is_domestic_open}, RealtimeManager={realtime_manager.running}")
async def persist_market_data_job():
"""
Flushes realtime_manager.price_cache to DB (StockItem).
"""
if not realtime_manager.price_cache:
return
# Snapshot and clear (thread-safe ish in async single loop)
# Or just iterate. Python dict iteration is safe if not modifying keys.
# We'll take keys copy.
codes_to_process = list(realtime_manager.price_cache.keys())
if not codes_to_process:
return
async with SessionLocal() as session:
count = 0
for code in codes_to_process:
data = realtime_manager.price_cache.get(code)
if not data: continue
# Upsert StockItem?
# Or just update if exists. StockItem generally exists if synced.
# If not exists, we might create it.
price = float(data['price'])
change = float(data['change'])
rate = float(data['rate'])
# Simple Update
stmt = update(StockItem).where(StockItem.code == code).values(
price=price,
change=change,
changePercent=rate
)
await session.execute(stmt)
count += 1
await session.commit()
# logger.debug(f"Persisted {count} stock prices to DB.")
async def news_scrap_job():
# Placeholder for News Scraper
logger.info("Scheduler: Scraping Naver News (Placeholder)...")
pass
async def auto_trade_scan_job():
"""
Scans ReservedOrders and triggers actions if conditions are met.
"""
async with SessionLocal() as session:
# Fetch Monitoring Orders
stmt = select(ReservedOrder).where(ReservedOrder.status == "MONITORING")
result = await session.execute(stmt)
orders = result.scalars().all()
for order in orders:
# Check Price
current_price = 0
# 1. Try Realtime Cache
if order.stockCode in realtime_manager.price_cache:
current_price = float(realtime_manager.price_cache[order.stockCode]['price'])
else:
# 2. Try DB (StockItem)
# ... (omitted for speed, usually Cache covers if monitored)
pass
if current_price <= 0: continue
# Trigger Logic (Simple)
triggered = False
if order.monitoringType == "PRICE_TRIGGER":
# Buy if Price <= Trigger (Dip Buy) ?? OR Buy if Price >= Trigger (Breakout)?
# Usually define "Condition". Assuming Buy Lower for now or User defined.
# Let's assume TriggerPrice is "Target".
# If BUY -> Price <= Trigger?
# If SELL -> Price >= Trigger?
if order.type == "BUY" and current_price <= order.triggerPrice:
triggered = True
elif order.type == "SELL" and current_price >= order.triggerPrice:
triggered = True
elif order.monitoringType == "TRAILING_STOP":
if order.type == "SELL":
# Trailing Sell (High Water Mark)
if not order.highestPrice or current_price > order.highestPrice:
order.highestPrice = current_price
if order.highestPrice > 0 and order.trailingValue:
drop = order.highestPrice - current_price
triggered = False
if order.trailingType == "PERCENT":
drop_rate = (drop / order.highestPrice) * 100
if drop_rate >= order.trailingValue: triggered = True
else: # AMOUNT
if drop >= order.trailingValue: triggered = True
if triggered:
logger.info(f"TS SELL Triggered: High={order.highestPrice}, Curr={current_price}")
elif order.type == "BUY":
# Trailing Buy (Low Water Mark / Rebound)
# Initialize lowest if 0 (assuming stock price never 0, or use 99999999 default)
if not order.lowestPrice or order.lowestPrice == 0:
order.lowestPrice = current_price
if current_price < order.lowestPrice:
order.lowestPrice = current_price
if order.lowestPrice > 0 and order.trailingValue:
rise = current_price - order.lowestPrice
triggered = False
if order.trailingType == "PERCENT":
rise_rate = (rise / order.lowestPrice) * 100
if rise_rate >= order.trailingValue: triggered = True
else:
if rise >= order.trailingValue: triggered = True
if triggered:
logger.info(f"TS BUY Triggered: Low={order.lowestPrice}, Curr={current_price}")
if triggered:
if triggered:
logger.info(f"Order TRIGGERED! {order.stockName} {order.type} @ {current_price} (Target: {order.triggerPrice})")
# Execute Order
# res = await kis_client.place_order(...)
# Update Status
# order.status = "TRIGGERED" (or "COMPLETED" after exec)
pass
await session.commit()
def start_scheduler():
scheduler.add_job(market_check_job, 'interval', minutes=5)
scheduler.add_job(news_scrap_job, 'interval', minutes=10)
scheduler.add_job(auto_trade_scan_job, 'interval', minutes=1)
scheduler.add_job(persist_market_data_job, 'interval', seconds=10) # Persist every 10s
scheduler.start()
logger.info("Scheduler Started.")