first commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.models.stock import PriceHistory
|
||||
from app.services.market_data import market_data_service
|
||||
|
||||
logger = setup_logger("collector")
|
||||
|
||||
|
||||
class PriceCollector:
|
||||
def __init__(self) -> None:
|
||||
self._running = False
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
now = datetime.now()
|
||||
if now.weekday() >= 5:
|
||||
return False
|
||||
|
||||
market_open = time(settings.collector.market_open_hour, 0)
|
||||
market_close = time(settings.collector.market_close_hour, settings.collector.market_close_minute)
|
||||
return market_open <= now.time() <= market_close
|
||||
|
||||
async def collect_price(self, stock_code: str, db: Session) -> bool:
|
||||
if not self.is_market_open():
|
||||
return False
|
||||
|
||||
try:
|
||||
price_data = await market_data_service.get_current_price(stock_code)
|
||||
if not price_data:
|
||||
return False
|
||||
|
||||
now = datetime.now()
|
||||
record = PriceHistory(
|
||||
stock_code=stock_code,
|
||||
datetime=now,
|
||||
open=price_data.get("open_price", 0),
|
||||
high=price_data.get("high_price", 0),
|
||||
low=price_data.get("low_price", 0),
|
||||
close=price_data.get("current_price", 0),
|
||||
volume=price_data.get("volume", 0),
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
|
||||
logger.debug(
|
||||
"가격 수집: %s = %d원 (%+.2f%%)",
|
||||
stock_code,
|
||||
price_data.get("current_price", 0),
|
||||
price_data.get("change_rate", 0),
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("가격 수집 오류 (%s): %s", stock_code, e)
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
async def collect_all(self, db: Session) -> int:
|
||||
from app.models.stock import Stock
|
||||
|
||||
stocks = db.query(Stock).filter(Stock.is_active == True).all()
|
||||
count = 0
|
||||
for stock in stocks:
|
||||
if await self.collect_price(stock.code, db):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def get_latest_price(self, stock_code: str, db: Session) -> dict | None:
|
||||
record = (
|
||||
db.query(PriceHistory)
|
||||
.filter(PriceHistory.stock_code == stock_code)
|
||||
.order_by(PriceHistory.datetime.desc())
|
||||
.first()
|
||||
)
|
||||
if not record:
|
||||
return None
|
||||
return {
|
||||
"stock_code": record.stock_code,
|
||||
"current_price": record.close,
|
||||
"open": record.open,
|
||||
"high": record.high,
|
||||
"low": record.low,
|
||||
"volume": record.volume,
|
||||
"datetime": record.datetime.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
price_collector = PriceCollector()
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import SessionLocal
|
||||
from app.core.logger import setup_logger
|
||||
from app.engine.collector import price_collector
|
||||
from app.engine.strategy_engine import strategy_engine
|
||||
|
||||
logger = setup_logger("scheduler")
|
||||
|
||||
scheduler = AsyncIOScheduler(timezone="Asia/Seoul")
|
||||
|
||||
|
||||
async def collect_job() -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
count = await price_collector.collect_all(db)
|
||||
if count > 0:
|
||||
logger.debug("가격 수집 완료: %d개 종목", count)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def strategy_job() -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
signals = await strategy_engine.evaluate_all(db)
|
||||
if signals:
|
||||
await strategy_engine.execute_signals(signals, db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def reset_daily_count_job() -> None:
|
||||
strategy_engine.reset_daily_count()
|
||||
logger.info("일일 매매 카운트 초기화")
|
||||
|
||||
|
||||
def start_scheduler() -> None:
|
||||
scheduler.add_job(
|
||||
collect_job,
|
||||
trigger=IntervalTrigger(seconds=settings.collector.interval_seconds),
|
||||
id="price_collector",
|
||||
name="주가 수집",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
scheduler.add_job(
|
||||
strategy_job,
|
||||
trigger=IntervalTrigger(seconds=settings.strategy.check_interval_seconds),
|
||||
id="strategy_engine",
|
||||
name="전략 실행",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
scheduler.add_job(
|
||||
reset_daily_count_job,
|
||||
trigger="cron",
|
||||
hour=0,
|
||||
minute=0,
|
||||
id="daily_reset",
|
||||
name="일일 카운트 초기화",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
scheduler.start()
|
||||
logger.info(
|
||||
"스케줄러 시작 - 수집: %d초 간격, 전략: %d초 간격",
|
||||
settings.collector.interval_seconds,
|
||||
settings.strategy.check_interval_seconds,
|
||||
)
|
||||
|
||||
|
||||
def stop_scheduler() -> None:
|
||||
if scheduler.running:
|
||||
scheduler.shutdown(wait=False)
|
||||
logger.info("스케줄러 종료")
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.core.logger import setup_logger
|
||||
|
||||
logger = setup_logger("strategy")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signal:
|
||||
action: str # buy / sell / hold
|
||||
stock_code: str
|
||||
qty: int = 0
|
||||
price: int = 0
|
||||
reason: str = ""
|
||||
confidence: float = 0.0
|
||||
strategy_id: int | None = None
|
||||
|
||||
|
||||
class BaseStrategy(ABC):
|
||||
def __init__(self, stock_code: str, params: dict, strategy_id: int = 0) -> None:
|
||||
self.stock_code = stock_code
|
||||
self.params = params
|
||||
self.strategy_id = strategy_id
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
@abstractmethod
|
||||
def evaluate(self, current_price: dict, price_history: list[dict], holding_qty: int) -> Signal:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _to_dataframe(prices: list[dict]) -> pd.DataFrame:
|
||||
if not prices:
|
||||
return pd.DataFrame()
|
||||
df = pd.DataFrame(prices)
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.engine.strategies.base import BaseStrategy, Signal
|
||||
|
||||
|
||||
class ConditionalStrategy(BaseStrategy):
|
||||
"""조건부 지정가/시장가 전략"""
|
||||
|
||||
def evaluate(self, current_price: dict, price_history: list[dict], holding_qty: int) -> Signal:
|
||||
price = current_price.get("current_price", 0)
|
||||
if price <= 0:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
buy_price = self.params.get("buy_price", 0)
|
||||
sell_price = self.params.get("sell_price", 0)
|
||||
change_rate_limit = self.params.get("change_rate_limit", 0)
|
||||
qty = self.params.get("qty", 1)
|
||||
|
||||
if change_rate_limit:
|
||||
change_rate = current_price.get("change_rate", 0)
|
||||
if change_rate <= -change_rate_limit and price > 0:
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"하락률 조건 충족: {change_rate:.2f}% <= -{change_rate_limit}%",
|
||||
confidence=min(abs(change_rate) / change_rate_limit, 1.0),
|
||||
)
|
||||
if change_rate >= change_rate_limit and holding_qty > 0:
|
||||
return Signal(
|
||||
action="sell",
|
||||
stock_code=self.stock_code,
|
||||
qty=min(qty, holding_qty),
|
||||
price=price,
|
||||
reason=f"상승률 조건 충족: {change_rate:.2f}% >= {change_rate_limit}%",
|
||||
confidence=min(abs(change_rate) / change_rate_limit, 1.0),
|
||||
)
|
||||
|
||||
if buy_price and price <= buy_price and holding_qty == 0:
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"매수 조건 충족: 현재가 {price} <= 목표가 {buy_price}",
|
||||
confidence=1.0,
|
||||
)
|
||||
|
||||
if sell_price and price >= sell_price and holding_qty > 0:
|
||||
return Signal(
|
||||
action="sell",
|
||||
stock_code=self.stock_code,
|
||||
qty=min(qty, holding_qty),
|
||||
price=price,
|
||||
reason=f"매도 조건 충족: 현재가 {price} >= 목표가 {sell_price}",
|
||||
confidence=1.0,
|
||||
)
|
||||
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.engine.strategies.base import BaseStrategy, Signal
|
||||
|
||||
|
||||
class PeriodicStrategy(BaseStrategy):
|
||||
"""정액(DCA) / 정률 투자 전략"""
|
||||
|
||||
def evaluate(self, current_price: dict, price_history: list[dict], holding_qty: int) -> Signal:
|
||||
price = current_price.get("current_price", 0)
|
||||
if price <= 0:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
invest_type = self.params.get("invest_type", "fixed_amount")
|
||||
amount = self.params.get("amount", 100000)
|
||||
ratio = self.params.get("ratio", 0.0)
|
||||
invest_days = self.params.get("invest_days", [0, 1, 2, 3, 4])
|
||||
invest_hour = self.params.get("invest_hour", 10)
|
||||
invest_minute = self.params.get("invest_minute", 0)
|
||||
min_price_drop = self.params.get("min_price_drop_percent", 0)
|
||||
max_price = self.params.get("max_price", 0)
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
if now.weekday() not in invest_days:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
if now.hour != invest_hour or now.minute != invest_minute:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
if max_price and price > max_price:
|
||||
return Signal(
|
||||
action="hold",
|
||||
stock_code=self.stock_code,
|
||||
reason=f"가격 상한 초과: {price} > {max_price}",
|
||||
)
|
||||
|
||||
if min_price_drop and len(price_history) >= 2:
|
||||
prev_close = price_history[-2].get("close", price)
|
||||
if prev_close > 0:
|
||||
drop_pct = (prev_close - price) / prev_close * 100
|
||||
if drop_pct < min_price_drop:
|
||||
return Signal(
|
||||
action="hold",
|
||||
stock_code=self.stock_code,
|
||||
reason=f"가격 하락 미충족: {drop_pct:.2f}% < {min_price_drop}%",
|
||||
)
|
||||
|
||||
if invest_type == "fixed_amount":
|
||||
qty = max(1, amount // price)
|
||||
elif invest_type == "fixed_ratio":
|
||||
total_invest = self.params.get("total_capital", 100_000_000)
|
||||
invest_amount = int(total_invest * ratio)
|
||||
qty = max(1, invest_amount // price)
|
||||
else:
|
||||
qty = 1
|
||||
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"정기투자: {invest_type}, {qty}주",
|
||||
confidence=1.0,
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ta
|
||||
import pandas as pd
|
||||
|
||||
from app.engine.strategies.base import BaseStrategy, Signal
|
||||
|
||||
|
||||
class TechnicalStrategy(BaseStrategy):
|
||||
"""기술적 분석 기반 전략 (MACD, RSI, 볼린저밴드)"""
|
||||
|
||||
def evaluate(self, current_price: dict, price_history: list[dict], holding_qty: int) -> Signal:
|
||||
df = self._to_dataframe(price_history)
|
||||
if len(df) < 30:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
price = current_price.get("current_price", 0)
|
||||
indicators = self.params.get("indicators", ["rsi"])
|
||||
qty = self.params.get("qty", 1)
|
||||
|
||||
buy_signals = []
|
||||
sell_signals = []
|
||||
|
||||
if "rsi" in indicators:
|
||||
rsi_signal = self._evaluate_rsi(df, price, holding_qty, qty)
|
||||
if rsi_signal.action == "buy":
|
||||
buy_signals.append(rsi_signal)
|
||||
elif rsi_signal.action == "sell":
|
||||
sell_signals.append(rsi_signal)
|
||||
|
||||
if "macd" in indicators:
|
||||
macd_signal = self._evaluate_macd(df, price, holding_qty, qty)
|
||||
if macd_signal.action == "buy":
|
||||
buy_signals.append(macd_signal)
|
||||
elif macd_signal.action == "sell":
|
||||
sell_signals.append(macd_signal)
|
||||
|
||||
if "bollinger" in indicators:
|
||||
bb_signal = self._evaluate_bollinger(df, price, holding_qty, qty)
|
||||
if bb_signal.action == "buy":
|
||||
buy_signals.append(bb_signal)
|
||||
elif bb_signal.action == "sell":
|
||||
sell_signals.append(bb_signal)
|
||||
|
||||
if buy_signals:
|
||||
best = max(buy_signals, key=lambda s: s.confidence)
|
||||
return best
|
||||
if sell_signals:
|
||||
best = max(sell_signals, key=lambda s: s.confidence)
|
||||
return best
|
||||
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
def _evaluate_rsi(
|
||||
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
|
||||
) -> Signal:
|
||||
period = self.params.get("rsi_period", 14)
|
||||
oversold = self.params.get("rsi_oversold", 30)
|
||||
overbought = self.params.get("rsi_overbought", 70)
|
||||
|
||||
rsi = ta.momentum.RSIIndicator(df["close"], window=period).rsi()
|
||||
current_rsi = rsi.iloc[-1] if not rsi.empty else 50
|
||||
|
||||
if current_rsi <= oversold and holding_qty == 0:
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"RSI 과매도: {current_rsi:.1f} <= {oversold}",
|
||||
confidence=(oversold - current_rsi) / oversold if oversold > 0 else 0,
|
||||
)
|
||||
|
||||
if current_rsi >= overbought and holding_qty > 0:
|
||||
return Signal(
|
||||
action="sell",
|
||||
stock_code=self.stock_code,
|
||||
qty=min(qty, holding_qty),
|
||||
price=price,
|
||||
reason=f"RSI 과매수: {current_rsi:.1f} >= {overbought}",
|
||||
confidence=(current_rsi - overbought) / (100 - overbought) if overbought < 100 else 0,
|
||||
)
|
||||
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
def _evaluate_macd(
|
||||
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
|
||||
) -> Signal:
|
||||
fast = self.params.get("macd_fast", 12)
|
||||
slow = self.params.get("macd_slow", 26)
|
||||
signal_period = self.params.get("macd_signal", 9)
|
||||
|
||||
macd_ind = ta.trend.MACD(df["close"], window_fast=fast, window_slow=slow, window_sign=signal_period)
|
||||
macd_line = macd_ind.macd()
|
||||
signal_line = macd_ind.macd_signal()
|
||||
|
||||
if len(macd_line) < 2 or len(signal_line) < 2:
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
prev_macd = macd_line.iloc[-2]
|
||||
prev_signal = signal_line.iloc[-2]
|
||||
curr_macd = macd_line.iloc[-1]
|
||||
curr_signal = signal_line.iloc[-1]
|
||||
|
||||
if prev_macd <= prev_signal and curr_macd > curr_signal and holding_qty == 0:
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"MACD 골든크로스: MACD({curr_macd:.2f}) > Signal({curr_signal:.2f})",
|
||||
confidence=0.8,
|
||||
)
|
||||
|
||||
if prev_macd >= prev_signal and curr_macd < curr_signal and holding_qty > 0:
|
||||
return Signal(
|
||||
action="sell",
|
||||
stock_code=self.stock_code,
|
||||
qty=min(qty, holding_qty),
|
||||
price=price,
|
||||
reason=f"MACD 데드크로스: MACD({curr_macd:.2f}) < Signal({curr_signal:.2f})",
|
||||
confidence=0.8,
|
||||
)
|
||||
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
|
||||
def _evaluate_bollinger(
|
||||
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
|
||||
) -> Signal:
|
||||
period = self.params.get("bb_period", 20)
|
||||
std_dev = self.params.get("bb_std", 2.0)
|
||||
|
||||
bb = ta.volatility.BollingerBands(df["close"], window=period, window_dev=std_dev)
|
||||
upper = bb.bollinger_hband().iloc[-1]
|
||||
lower = bb.bollinger_lband().iloc[-1]
|
||||
mid = bb.bollinger_mavg().iloc[-1]
|
||||
|
||||
if price <= lower and holding_qty == 0:
|
||||
return Signal(
|
||||
action="buy",
|
||||
stock_code=self.stock_code,
|
||||
qty=qty,
|
||||
price=price,
|
||||
reason=f"볼린저밴드 하단 돌파: 가격({price}) <= 하단({lower:.0f})",
|
||||
confidence=0.7,
|
||||
)
|
||||
|
||||
if price >= upper and holding_qty > 0:
|
||||
return Signal(
|
||||
action="sell",
|
||||
stock_code=self.stock_code,
|
||||
qty=min(qty, holding_qty),
|
||||
price=price,
|
||||
reason=f"볼린저밴드 상단 돌파: 가격({price}) >= 상단({upper:.0f})",
|
||||
confidence=0.7,
|
||||
)
|
||||
|
||||
return Signal(action="hold", stock_code=self.stock_code)
|
||||
@@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.models.stock import PriceHistory, Strategy, Trade
|
||||
from app.services.market_data import market_data_service
|
||||
from app.services.trading import trading_service
|
||||
from app.engine.strategies.base import BaseStrategy, Signal
|
||||
from app.engine.strategies.conditional import ConditionalStrategy
|
||||
from app.engine.strategies.technical import TechnicalStrategy
|
||||
from app.engine.strategies.periodic import PeriodicStrategy
|
||||
|
||||
logger = setup_logger("strategy_engine")
|
||||
|
||||
STRATEGY_MAP = {
|
||||
"conditional": ConditionalStrategy,
|
||||
"technical": TechnicalStrategy,
|
||||
"periodic": PeriodicStrategy,
|
||||
}
|
||||
|
||||
|
||||
class StrategyEngine:
|
||||
def __init__(self) -> None:
|
||||
self._daily_trade_count = 0
|
||||
|
||||
def _create_strategy(self, strategy_record: Strategy) -> BaseStrategy | None:
|
||||
cls = STRATEGY_MAP.get(strategy_record.strategy_type)
|
||||
if not cls:
|
||||
logger.warning("알 수 없는 전략 타입: %s", strategy_record.strategy_type)
|
||||
return None
|
||||
|
||||
params = json.loads(strategy_record.params_json) if strategy_record.params_json else {}
|
||||
return cls(
|
||||
stock_code=strategy_record.stock_code,
|
||||
params=params,
|
||||
strategy_id=strategy_record.id,
|
||||
)
|
||||
|
||||
async def evaluate_all(self, db: Session) -> list[Signal]:
|
||||
strategies = db.query(Strategy).filter(Strategy.is_active == True).all()
|
||||
signals: list[Signal] = []
|
||||
|
||||
for strat_record in strategies:
|
||||
try:
|
||||
strategy = self._create_strategy(strat_record)
|
||||
if not strategy:
|
||||
continue
|
||||
|
||||
current_price = await market_data_service.get_current_price(strat_record.stock_code)
|
||||
if not current_price:
|
||||
continue
|
||||
|
||||
price_history = self._get_price_history(db, strat_record.stock_code)
|
||||
holding = self._get_holding_qty(db, strat_record.stock_code)
|
||||
|
||||
signal = strategy.evaluate(current_price, price_history, holding)
|
||||
if signal.action != "hold":
|
||||
signal.qty = max(signal.qty, strat_record.qty)
|
||||
signal.strategy_id = strat_record.id
|
||||
signals.append(signal)
|
||||
logger.info(
|
||||
"신호 발생: %s %s %s주 - %s",
|
||||
signal.stock_code,
|
||||
signal.action,
|
||||
signal.qty,
|
||||
signal.reason,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("전략 평가 오류 (ID=%s): %s", strat_record.id, e)
|
||||
|
||||
return signals
|
||||
|
||||
async def execute_signals(self, signals: list[Signal], db: Session) -> list[Trade]:
|
||||
if self._daily_trade_count >= settings.strategy.max_daily_trades:
|
||||
logger.warning("일일 최대 매매 횟수 초과 (%d)", settings.strategy.max_daily_trades)
|
||||
return []
|
||||
|
||||
trades: list[Trade] = []
|
||||
for signal in signals:
|
||||
try:
|
||||
order_result = await trading_service.place_order(
|
||||
stock_code=signal.stock_code,
|
||||
side=signal.action,
|
||||
qty=signal.qty,
|
||||
price=signal.price,
|
||||
order_type=settings.strategy.default_order_type,
|
||||
)
|
||||
|
||||
stock_name = ""
|
||||
price_data = await market_data_service.get_current_price(signal.stock_code)
|
||||
if price_data:
|
||||
stock_name = price_data.get("stock_name", "")
|
||||
|
||||
trade = Trade(
|
||||
order_no=order_result.get("order_no", ""),
|
||||
stock_code=signal.stock_code,
|
||||
stock_name=stock_name,
|
||||
side=signal.action,
|
||||
qty=signal.qty,
|
||||
price=signal.price,
|
||||
order_type=settings.strategy.default_order_type,
|
||||
status="filled" if order_result.get("rt_cd") == "0" else "rejected",
|
||||
strategy_id=signal.strategy_id,
|
||||
)
|
||||
db.add(trade)
|
||||
db.commit()
|
||||
self._daily_trade_count += 1
|
||||
trades.append(trade)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("주문 실행 오류: %s", e)
|
||||
|
||||
return trades
|
||||
|
||||
def _get_price_history(self, db: Session, stock_code: str, limit: int = 60) -> list[dict]:
|
||||
records = (
|
||||
db.query(PriceHistory)
|
||||
.filter(PriceHistory.stock_code == stock_code)
|
||||
.order_by(PriceHistory.datetime.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"date": r.datetime.strftime("%Y%m%d"),
|
||||
"open": r.open,
|
||||
"high": r.high,
|
||||
"low": r.low,
|
||||
"close": r.close,
|
||||
"volume": r.volume,
|
||||
}
|
||||
for r in reversed(records)
|
||||
]
|
||||
|
||||
def _get_holding_qty(self, db: Session, stock_code: str) -> int:
|
||||
from app.models.stock import Holding
|
||||
holding = db.query(Holding).filter(Holding.stock_code == stock_code).first()
|
||||
return holding.qty if holding else 0
|
||||
|
||||
def reset_daily_count(self) -> None:
|
||||
self._daily_trade_count = 0
|
||||
|
||||
|
||||
strategy_engine = StrategyEngine()
|
||||
Reference in New Issue
Block a user