galaxis-po/backend/jobs/kjb_signal_job.py
zephyrdark 3c969fc53c feat: wire KJB into backtest worker, add Signal API, add scheduler job
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:16:13 +09:00

110 lines
3.4 KiB
Python

"""
Daily KJB signal generation job.
"""
import logging
from datetime import date, timedelta
import pandas as pd
from app.core.database import SessionLocal
from app.models.stock import Stock, Price
from app.models.signal import Signal, SignalType, SignalStatus
from app.services.strategy.kjb import KJBSignalGenerator
logger = logging.getLogger(__name__)
def run_kjb_signals():
"""
Generate KJB trading signals for today.
Called by scheduler at 18:15 KST (after price collection).
"""
logger.info("Starting KJB signal generation")
db = SessionLocal()
try:
today = date.today()
signal_gen = KJBSignalGenerator()
stocks = (
db.query(Stock)
.filter(Stock.market_cap.isnot(None))
.order_by(Stock.market_cap.desc())
.limit(30)
.all()
)
tickers = [s.ticker for s in stocks]
name_map = {s.ticker: s.name for s in stocks}
lookback_start = today - timedelta(days=90)
kospi_prices = (
db.query(Price)
.filter(Price.ticker == "069500")
.filter(Price.date >= lookback_start, Price.date <= today)
.order_by(Price.date)
.all()
)
if not kospi_prices:
logger.warning("No KOSPI data available for signal generation")
return
kospi_df = pd.DataFrame([
{"date": p.date, "close": float(p.close)}
for p in kospi_prices
]).set_index("date")
signals_created = 0
for ticker in tickers:
stock_prices = (
db.query(Price)
.filter(Price.ticker == ticker)
.filter(Price.date >= lookback_start, Price.date <= today)
.order_by(Price.date)
.all()
)
if len(stock_prices) < 21:
continue
stock_df = pd.DataFrame([{
"date": p.date,
"open": float(p.open),
"high": float(p.high),
"low": float(p.low),
"close": float(p.close),
"volume": int(p.volume),
} for p in stock_prices]).set_index("date")
signals = signal_gen.generate_signals(stock_df, kospi_df)
if today in signals.index and signals.loc[today, "buy"]:
close_price = stock_df.loc[today, "close"]
reason_parts = []
if signals.loc[today, "breakout"]:
reason_parts.append("breakout")
if signals.loc[today, "large_candle"]:
reason_parts.append("large_candle")
signal = Signal(
date=today,
ticker=ticker,
name=name_map.get(ticker),
signal_type=SignalType.BUY,
entry_price=close_price,
target_price=round(close_price * 1.05, 2),
stop_loss_price=round(close_price * 0.97, 2),
reason=", ".join(reason_parts),
status=SignalStatus.ACTIVE,
)
db.add(signal)
signals_created += 1
db.commit()
logger.info(f"KJB signal generation complete: {signals_created} buy signals")
except Exception as e:
logger.exception(f"KJB signal generation failed: {e}")
finally:
db.close()