날짜 수정
This commit is contained in:
@@ -45,8 +45,9 @@ from app.core.config import settings
|
||||
from app.core.json_helpers import _dumps
|
||||
from app.services.websocket_manager import websocket_manager
|
||||
from app.core.database import get_db, AsyncSessionLocal
|
||||
from app.models.models import TradeHistory
|
||||
from app.models.models import TradeHistory, ActiveTrade
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update as sa_update
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -263,8 +264,11 @@ async def run_day_trading_cycle() -> Dict[str, Any]:
|
||||
|
||||
result["phase"] = "active"
|
||||
|
||||
# 사이클 단위 분봉 캐시 — 청산 체크와 신규 진입에서 동일 종목 중복 fetch 방지
|
||||
_cycle_candle_cache: Dict[str, List[MinuteCandle]] = {}
|
||||
|
||||
# ── 4. 활성 포지션 청산 체크 ──
|
||||
exits = await _check_exits()
|
||||
exits = await _check_exits(candle_cache=_cycle_candle_cache)
|
||||
result["exits"] = exits
|
||||
|
||||
# ── 5. 신규 진입 신호 체크 ──
|
||||
@@ -281,6 +285,7 @@ async def run_day_trading_cycle() -> Dict[str, Any]:
|
||||
total_assets=total_assets,
|
||||
available_cash=available_cash,
|
||||
current_day_trade_amount=current_day_trade_amount,
|
||||
candle_cache=_cycle_candle_cache,
|
||||
)
|
||||
result["signals"] = [
|
||||
{
|
||||
@@ -329,8 +334,14 @@ async def _collect_opening_range() -> None:
|
||||
await asyncio.sleep(0.6)
|
||||
|
||||
|
||||
async def _check_exits() -> List[Dict[str, Any]]:
|
||||
"""활성 포지션 청산 조건 체크 및 실행"""
|
||||
async def _check_exits(
|
||||
candle_cache: Optional[Dict[str, List[MinuteCandle]]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""활성 포지션 청산 조건 체크 및 실행
|
||||
|
||||
Args:
|
||||
candle_cache: 사이클 단위 분봉 캐시 {stock_code: candles}. None이면 직접 fetch.
|
||||
"""
|
||||
global _daily_pnl
|
||||
exits = []
|
||||
now_time = _kst_time()
|
||||
@@ -345,7 +356,12 @@ async def _check_exits() -> List[Dict[str, Any]]:
|
||||
if current_price > pos["highest_price"]:
|
||||
_positions[code]["highest_price"] = current_price
|
||||
|
||||
candles = await _fetch_candles(code)
|
||||
if candle_cache is not None and code in candle_cache:
|
||||
candles = candle_cache[code]
|
||||
else:
|
||||
candles = await _fetch_candles(code)
|
||||
if candle_cache is not None:
|
||||
candle_cache[code] = candles
|
||||
candles_5m = aggregate_to_5min(candles)
|
||||
|
||||
exit_signal = check_day_trade_exit(
|
||||
@@ -371,6 +387,12 @@ async def _check_exits() -> List[Dict[str, Any]]:
|
||||
db, code, pos["stock_name"], "SELL",
|
||||
current_price, quantity, exit_signal.reason, pnl,
|
||||
)
|
||||
await db.execute(
|
||||
sa_update(ActiveTrade)
|
||||
.where(ActiveTrade.stock_code == code, ActiveTrade.strategy_name == "day_trade", ActiveTrade.status == "active")
|
||||
.values(status="closed")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
exits.append({
|
||||
"stock_code": code,
|
||||
@@ -399,14 +421,24 @@ async def _scan_signals(
|
||||
total_assets: float,
|
||||
available_cash: float,
|
||||
current_day_trade_amount: float,
|
||||
candle_cache: Optional[Dict[str, List[MinuteCandle]]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""감시 종목에서 진입 신호 스캔"""
|
||||
"""감시 종목에서 진입 신호 스캔
|
||||
|
||||
Args:
|
||||
candle_cache: 사이클 단위 분봉 캐시 {stock_code: candles}. None이면 직접 fetch.
|
||||
"""
|
||||
results = []
|
||||
for code, name in list(_watchlist.items()):
|
||||
if code in _positions:
|
||||
continue
|
||||
try:
|
||||
candles_1m = await _fetch_candles(code)
|
||||
if candle_cache is not None and code in candle_cache:
|
||||
candles_1m = candle_cache[code]
|
||||
else:
|
||||
candles_1m = await _fetch_candles(code)
|
||||
if candle_cache is not None:
|
||||
candle_cache[code] = candles_1m
|
||||
if not candles_1m:
|
||||
continue
|
||||
candles_5m = aggregate_to_5min(candles_1m)
|
||||
@@ -486,6 +518,21 @@ async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict
|
||||
db, signal.stock_code, signal.stock_name, "BUY",
|
||||
signal.entry_price, quantity, signal.reason,
|
||||
)
|
||||
# ActiveTrade 저장 — 재시작 시 포지션 복구를 위해
|
||||
active = ActiveTrade(
|
||||
stock_code=signal.stock_code,
|
||||
stock_name=signal.stock_name,
|
||||
quantity=quantity,
|
||||
entry_price=signal.entry_price,
|
||||
stop_loss=signal.stop_loss,
|
||||
take_profit=signal.target_price,
|
||||
highest_since_entry=signal.entry_price,
|
||||
strategy_name="day_trade",
|
||||
entry_reason=signal.strategy,
|
||||
status="active",
|
||||
)
|
||||
db.add(active)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
f"🛒 데이트레이딩 진입 [{signal.stock_code}] "
|
||||
@@ -519,11 +566,69 @@ async def _execute_eod_flat() -> None:
|
||||
_daily_pnl += pnl
|
||||
del _positions[code]
|
||||
logger.info(f"🔔 EOD 강제 청산 [{code}]: PNL={pnl:+,.0f}원")
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await _record_trade(
|
||||
db, code, pos["stock_name"], "SELL",
|
||||
current_price, pos["quantity"], "EOD_flat", pnl,
|
||||
)
|
||||
await db.execute(
|
||||
sa_update(ActiveTrade)
|
||||
.where(ActiveTrade.stock_code == code, ActiveTrade.strategy_name == "day_trade", ActiveTrade.status == "active")
|
||||
.values(status="closed")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
await websocket_manager.broadcast(_dumps({
|
||||
"type": "day_trade_exit",
|
||||
"stock_code": code,
|
||||
"reason": "EOD_flat",
|
||||
"pnl": pnl,
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.error(f"EOD 청산 오류 [{code}]: {e}")
|
||||
await asyncio.sleep(0.6)
|
||||
|
||||
|
||||
async def restore_positions_from_db() -> None:
|
||||
"""앱 재시작 후 오늘 데이트레이딩 포지션 DB 복구.
|
||||
|
||||
strategy_name='day_trade', status='active'인 당일 ActiveTrade 레코드로
|
||||
인메모리 _positions를 재구성한다.
|
||||
"""
|
||||
if _positions:
|
||||
return # 이미 포지션 있으면 스킵
|
||||
today = _today_kst()
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(
|
||||
select(ActiveTrade).where(
|
||||
ActiveTrade.strategy_name == "day_trade",
|
||||
ActiveTrade.status == "active",
|
||||
)
|
||||
)
|
||||
rows = result.scalars().all()
|
||||
for row in rows:
|
||||
if row.created_at and row.created_at.date() != today:
|
||||
continue
|
||||
entry_price = float(row.entry_price)
|
||||
_positions[row.stock_code] = {
|
||||
"stock_name": row.stock_name or row.stock_code,
|
||||
"strategy": row.entry_reason or "unknown",
|
||||
"entry_price": entry_price,
|
||||
"stop_loss": float(row.stop_loss) if row.stop_loss else entry_price * 0.97,
|
||||
"target_price": float(row.take_profit) if row.take_profit else entry_price * 1.03,
|
||||
"quantity": row.quantity,
|
||||
"highest_price": float(row.highest_since_entry or row.entry_price),
|
||||
"entry_time": row.created_at.strftime("%H:%M:%S") if row.created_at else "??:??:??",
|
||||
"reason": row.entry_reason or "",
|
||||
}
|
||||
if _positions:
|
||||
logger.info(f"📋 데이트레이딩 포지션 복구: {list(_positions.keys())}")
|
||||
except Exception as e:
|
||||
logger.error(f"데이트레이딩 포지션 복구 실패: {e}")
|
||||
|
||||
|
||||
# ── API 엔드포인트 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/watchlist")
|
||||
|
||||
Reference in New Issue
Block a user