ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -0,0 +1,500 @@
"""
kis_trader/database/paper_store.py — 페이퍼 매매 전용 테이블 (실매와 완전 분리)
실매 ``active_trades`` / ``orders`` / ``trade_history`` 와 스키마를 맞추되
테이블을 분리해 보유·매도·고아복구·브로커 대조에 절대 섞이지 않게 한다.
- active_trades_paper
- orders_paper
- trade_history_paper
"""
from __future__ import annotations
import datetime
import logging
import uuid
from typing import Any, Dict, List, Optional
logger = logging.getLogger("kis_trader.paper_store")
def ensure_paper_tables(db) -> None:
"""TradeDB.conn 에 paper 3테이블 생성 (없으면)."""
conn = db.conn
with conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS active_trades_paper (
code VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL,
strategy VARCHAR(50) NOT NULL DEFAULT 'MANUAL',
PRIMARY KEY (code, strategy),
avg_buy_price DOUBLE NOT NULL,
current_price DOUBLE,
stop_price DOUBLE,
target_price DOUBLE,
max_price DOUBLE,
atr_entry DOUBLE,
target_qty INT NOT NULL,
current_qty INT NOT NULL,
total_invested DOUBLE,
status VARCHAR(20) NOT NULL,
buy_date VARCHAR(30) NOT NULL,
updated_at VARCHAR(30) NOT NULL,
size_class VARCHAR(20),
fill_mode VARCHAR(16) NOT NULL DEFAULT 'paper',
note VARCHAR(200) DEFAULT NULL
) CHARACTER SET utf8mb4
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS orders_paper (
id BIGINT NOT NULL AUTO_INCREMENT,
ord_no VARCHAR(40) NOT NULL,
strategy_id VARCHAR(40) NOT NULL,
code VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL DEFAULT '',
side VARCHAR(8) NOT NULL,
qty INT NOT NULL,
price DOUBLE DEFAULT 0,
filled_qty INT NOT NULL DEFAULT 0,
filled_avg_price DOUBLE DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'FILLED',
msg_cd VARCHAR(20) DEFAULT NULL,
msg1 VARCHAR(300) DEFAULT NULL,
ord_date VARCHAR(10) NOT NULL,
submitted_at VARCHAR(30) NOT NULL,
filled_at VARCHAR(30) DEFAULT NULL,
raw_json MEDIUMTEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_paper_ord_no_ctx (ord_no, strategy_id, code, side, ord_date),
INDEX idx_paper_strategy_date (strategy_id, ord_date),
INDEX idx_paper_code_date (code, ord_date)
) CHARACTER SET utf8mb4
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS trade_history_paper (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL,
strategy VARCHAR(50),
buy_price DOUBLE NOT NULL,
sell_price DOUBLE NOT NULL,
qty INT NOT NULL,
profit_rate DOUBLE NOT NULL,
realized_pnl DOUBLE NOT NULL,
hold_minutes INT,
buy_date VARCHAR(30),
sell_date VARCHAR(30) NOT NULL,
sell_reason VARCHAR(200),
env_snapshot TEXT,
size_class VARCHAR(20),
fill_mode VARCHAR(16) NOT NULL DEFAULT 'paper',
INDEX idx_thp_strategy_sell (strategy, sell_date),
INDEX idx_thp_code_sell (code, sell_date)
) CHARACTER SET utf8mb4
"""
)
def _new_paper_ord_no(side: str) -> str:
side_u = (side or "X").upper()[:1]
ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"PAPER-{side_u}{ts}-{uuid.uuid4().hex[:8]}"
def insert_order_paper(
db,
*,
strategy_id: str,
code: str,
name: str,
side: str,
qty: int,
price: float,
filled_qty: Optional[int] = None,
status: str = "FILLED",
msg1: str = "paper",
) -> str:
"""즉시 체결 가정으로 orders_paper INSERT. 반환: ord_no."""
ensure_paper_tables(db)
side_u = (side or "").upper()
assert side_u in ("BUY", "SELL"), side_u
now = datetime.datetime.now()
ord_no = _new_paper_ord_no(side_u)
fq = int(filled_qty if filled_qty is not None else qty)
with db.conn:
db.conn.execute(
"""
INSERT INTO orders_paper (
ord_no, strategy_id, code, name, side, qty, price,
filled_qty, filled_avg_price, status, msg1,
ord_date, submitted_at, filled_at
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""",
(
ord_no,
strategy_id,
code,
name or code,
side_u,
int(qty),
float(price or 0),
fq,
float(price or 0),
status,
msg1,
now.strftime("%Y-%m-%d"),
now.strftime("%Y-%m-%d %H:%M:%S"),
now.strftime("%Y-%m-%d %H:%M:%S"),
),
)
return ord_no
def upsert_active_trade_paper(db, trade_data: Dict[str, Any]) -> bool:
"""active_trades_paper UPSERT (실매 upsert_trade 와 동일 의미, ML컬럼 생략)."""
ensure_paper_tables(db)
code = str(trade_data.get("code") or "").strip()
if not code:
return False
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
from kis_trader.utils.strategy_ids import canonical_strategy_id
strategy = canonical_strategy_id(trade_data.get("strategy", "MANUAL"))
except Exception:
strategy = trade_data.get("strategy", "MANUAL") or "MANUAL"
avg = float(trade_data.get("avg_buy_price") or trade_data.get("buy_price") or 0)
qty = int(trade_data.get("current_qty") or trade_data.get("qty") or 0)
try:
with db.conn:
db.conn.execute(
"""
INSERT INTO active_trades_paper (
code, name, strategy, avg_buy_price, current_price,
stop_price, target_price, max_price, atr_entry,
target_qty, current_qty, total_invested, status,
buy_date, updated_at, size_class, fill_mode, note
) VALUES (
%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s
)
ON DUPLICATE KEY UPDATE
avg_buy_price = VALUES(avg_buy_price),
current_price = VALUES(current_price),
stop_price = COALESCE(VALUES(stop_price), stop_price),
target_price = COALESCE(VALUES(target_price), target_price),
atr_entry = COALESCE(VALUES(atr_entry), atr_entry),
current_qty = VALUES(current_qty),
target_qty = VALUES(target_qty),
total_invested = VALUES(total_invested),
max_price = GREATEST(COALESCE(max_price,0), COALESCE(VALUES(max_price),0)),
status = VALUES(status),
updated_at = VALUES(updated_at),
size_class = COALESCE(VALUES(size_class), size_class),
note = COALESCE(VALUES(note), note)
""",
(
code,
trade_data.get("name") or code,
strategy,
avg,
float(trade_data.get("current_price") or avg),
float(trade_data.get("stop_price") or 0),
float(trade_data.get("target_price") or 0),
float(trade_data.get("max_price") or avg),
float(trade_data.get("atr_entry") or trade_data.get("atr_at_entry") or 0),
int(trade_data.get("target_qty") or qty),
qty,
float(trade_data.get("total_invested") or (avg * qty)),
trade_data.get("status") or "HOLDING",
trade_data.get("buy_date") or now,
now,
trade_data.get("size_class") or "",
trade_data.get("fill_mode") or "paper",
trade_data.get("note"),
),
)
return True
except Exception as e:
logger.error("upsert_active_trade_paper 실패 (%s): %s", code, e)
return False
def get_active_trades_paper(
db, *, strategy_id: Optional[str] = None
) -> Dict[str, Dict[str, Any]]:
"""strategy 정확 일치(또는 전체). 반환 {code: holding_dict} — Base holdings 호환."""
ensure_paper_tables(db)
try:
if strategy_id:
rows = db.conn.execute(
"SELECT * FROM active_trades_paper WHERE strategy=%s",
(strategy_id,),
).fetchall()
else:
rows = db.conn.execute("SELECT * FROM active_trades_paper").fetchall()
out: Dict[str, Dict[str, Any]] = {}
for row in rows:
code = row["code"]
out[code] = {
"code": code,
"name": row["name"],
"strategy": row["strategy"],
"buy_price": row["avg_buy_price"],
"avg_buy_price": row["avg_buy_price"],
"current_price": row["current_price"],
"stop_price": row["stop_price"],
"target_price": row["target_price"],
"max_price": row["max_price"],
"atr_at_entry": row["atr_entry"],
"qty": row["current_qty"],
"target_qty": row["target_qty"],
"current_qty": row["current_qty"],
"total_invested": row["total_invested"],
"status": row["status"],
"buy_date": row["buy_date"],
"updated_at": row["updated_at"],
"size_class": row["size_class"] if "size_class" in row.keys() else None,
"paper": True,
}
return out
except Exception as e:
logger.error("get_active_trades_paper 실패: %s", e)
return {}
def close_trade_paper(
db,
*,
code: str,
strategy: str,
sell_price: float,
sell_reason: str = "",
realized_pnl_override: Optional[float] = None,
) -> bool:
"""active_trades_paper → trade_history_paper 이동 후 삭제."""
ensure_paper_tables(db)
try:
row = db.conn.execute(
"SELECT * FROM active_trades_paper WHERE code=%s AND strategy=%s",
(code, strategy),
).fetchone()
if not row:
logger.warning("close_trade_paper: %s/%s 없음", code, strategy)
return False
buy_price = float(row["avg_buy_price"] or 0)
qty = int(row["current_qty"] or 0)
if realized_pnl_override is not None:
realized_pnl = float(realized_pnl_override)
else:
realized_pnl = (float(sell_price) - buy_price) * qty
profit_rate = (
(realized_pnl / (buy_price * qty) * 100.0) if buy_price * qty > 0 else 0.0
)
try:
buy_time = datetime.datetime.strptime(
str(row["buy_date"])[:19], "%Y-%m-%d %H:%M:%S"
)
hold_minutes = int(
(datetime.datetime.now() - buy_time).total_seconds() / 60
)
except Exception:
hold_minutes = 0
sell_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
from kis_trader.utils.strategy_ids import canonical_strategy_id
hist_strategy = canonical_strategy_id(row.get("strategy") or strategy)
except Exception:
hist_strategy = row.get("strategy") or strategy
with db.conn:
db.conn.execute(
"""
INSERT INTO trade_history_paper (
code, name, strategy, buy_price, sell_price, qty,
profit_rate, realized_pnl, hold_minutes,
buy_date, sell_date, sell_reason, size_class, fill_mode
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""",
(
row["code"],
row["name"],
hist_strategy,
buy_price,
float(sell_price),
qty,
profit_rate,
realized_pnl,
hold_minutes,
row["buy_date"],
sell_time,
sell_reason or "paper",
row["size_class"] if "size_class" in row.keys() else None,
"paper",
),
)
db.conn.execute(
"DELETE FROM active_trades_paper WHERE code=%s AND strategy=%s",
(code, strategy),
)
return True
except Exception as e:
logger.error("close_trade_paper 실패 (%s): %s", code, e)
return False
def count_paper_trades_today(db, *, strategy_id: str, code: str, ymd: str) -> int:
"""ymd=YYYYMMDD — 당일 paper 청산 건수 (일일 매수 한도용)."""
ensure_paper_tables(db)
day = f"{ymd[:4]}-{ymd[4:6]}-{ymd[6:8]}" if len(ymd) == 8 else ymd
try:
row = db.conn.execute(
"""
SELECT COUNT(*) AS n FROM trade_history_paper
WHERE strategy=%s AND code=%s
AND sell_date >= %s AND sell_date <= %s
""",
(strategy_id, code, day + " 00:00:00", day + " 23:59:59"),
).fetchone()
return int(row["n"] if row else 0)
except Exception:
return 0
def list_trade_history_paper(
db,
*,
strategy_like: str,
start: str = "",
end: str = "",
) -> List[Dict[str, Any]]:
ensure_paper_tables(db)
params: List[Any] = [strategy_like]
sql = "SELECT * FROM trade_history_paper WHERE strategy LIKE %s"
if start:
sql += " AND sell_date >= %s"
params.append(start + " 00:00:00")
if end:
sql += " AND sell_date <= %s"
params.append(end + " 23:59:59")
sql += " ORDER BY sell_date ASC"
try:
rows = db.conn.execute(sql, tuple(params)).fetchall()
return [dict(r) for r in rows]
except Exception as e:
logger.error("list_trade_history_paper 실패: %s", e)
return []
def open_paper_buy(
db,
*,
strategy_id: str,
code: str,
name: str,
price: float,
qty: int,
stop_price: float = 0.0,
target_price: float = 0.0,
atr_entry: float = 0.0,
size_class: str = "",
entry_features: Optional[Dict] = None,
) -> Optional[str]:
"""
페이퍼 매수 1건: orders_paper + active_trades_paper.
성공 시 ord_no, 실패 시 None.
"""
if qty <= 0 or price <= 0:
return None
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
ord_no = insert_order_paper(
db,
strategy_id=strategy_id,
code=code,
name=name,
side="BUY",
qty=qty,
price=price,
msg1="paper_buy",
)
note = None
if entry_features:
try:
import json
note = json.dumps(entry_features, ensure_ascii=False)[:190]
except Exception:
note = "paper"
ok = upsert_active_trade_paper(
db,
{
"code": code,
"name": name,
"strategy": strategy_id,
"avg_buy_price": price,
"current_price": price,
"stop_price": stop_price,
"target_price": target_price,
"max_price": price,
"atr_entry": atr_entry,
"target_qty": qty,
"current_qty": qty,
"total_invested": price * qty,
"status": "HOLDING",
"buy_date": now,
"size_class": size_class or "",
"fill_mode": "paper",
"note": note,
},
)
if not ok:
return None
logger.info(
"📝 [PAPER BUY] %s %s @ %.4f × %d (ord=%s)",
strategy_id, code, price, qty, ord_no,
)
return ord_no
def open_paper_sell(
db,
*,
strategy_id: str,
code: str,
name: str,
qty: int,
sell_price: float,
sell_reason: str = "",
buy_price: float = 0.0,
) -> bool:
"""페이퍼 매도: orders_paper SELL + close_trade_paper."""
if qty <= 0 or sell_price <= 0:
return False
insert_order_paper(
db,
strategy_id=strategy_id,
code=code,
name=name,
side="SELL",
qty=qty,
price=sell_price,
msg1=sell_reason or "paper_sell",
)
pnl_override = None
if buy_price > 0:
pnl_override = (sell_price - buy_price) * qty
ok = close_trade_paper(
db,
code=code,
strategy=strategy_id,
sell_price=sell_price,
sell_reason=sell_reason or "paper",
realized_pnl_override=pnl_override,
)
if ok:
logger.info(
"📝 [PAPER SELL] %s %s @ %.4f × %d (%s)",
strategy_id, code, sell_price, qty, sell_reason or "",
)
return ok