feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -9,14 +9,16 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("kis_trader.env")
|
||||
|
||||
# 순환 import 방지용 레이지 TradeDB 참조
|
||||
_db_instance = None
|
||||
# get_merged_env_snapshot() — 프로세스당 1회 (백테·파라서치 env 키 반복 조회 가속)
|
||||
# get_merged_env_snapshot() — TTL 내 재사용 (실매: 웹에서 MAX_DAILY 등 변경 즉시 반영)
|
||||
_merged_env_cache: Optional[Dict[str, str]] = None
|
||||
_merged_env_cache_ts: float = 0.0
|
||||
# env 캐시 세대 카운터 — invalidate 시 +1. 파생 캐시(예: whipsaw 파라미터)가
|
||||
# 이 값으로 무효화를 감지해 안전하게 재계산한다(값은 그대로, 재계산 시점만 동일).
|
||||
_env_generation: int = 0
|
||||
@@ -43,11 +45,20 @@ def set_db(db_obj) -> None:
|
||||
|
||||
def invalidate_merged_env_cache() -> None:
|
||||
"""env/config 저장 후 스냅샷 캐시 무효화 (insert_env_snapshot 등)."""
|
||||
global _merged_env_cache, _env_generation
|
||||
global _merged_env_cache, _merged_env_cache_ts, _env_generation
|
||||
_merged_env_cache = None
|
||||
_merged_env_cache_ts = 0.0
|
||||
_env_generation += 1
|
||||
|
||||
|
||||
def _merged_env_cache_ttl_sec() -> float:
|
||||
"""DB env 스냅샷 메모리 TTL(초). os.environ 만 사용(순환 import 방지)."""
|
||||
try:
|
||||
return max(0.0, float(os.environ.get("ENV_MERGED_CACHE_TTL_SEC", "60")))
|
||||
except (ValueError, TypeError):
|
||||
return 60.0
|
||||
|
||||
|
||||
def env_cache_generation() -> int:
|
||||
"""현재 env 캐시 세대. invalidate 될 때마다 증가.
|
||||
|
||||
@@ -66,13 +77,15 @@ def _strip_comment(val: Any) -> Any:
|
||||
|
||||
def get_merged_env_dict() -> dict:
|
||||
"""env_config + config_scalp/short/momentum/… 최신 행 병합 flat dict."""
|
||||
global _merged_env_cache
|
||||
if _merged_env_cache is not None:
|
||||
global _merged_env_cache, _merged_env_cache_ts
|
||||
ttl = _merged_env_cache_ttl_sec()
|
||||
now = time.time()
|
||||
if _merged_env_cache is not None and (ttl <= 0 or (now - _merged_env_cache_ts) < ttl):
|
||||
return _merged_env_cache
|
||||
|
||||
db = _get_db()
|
||||
if db is None:
|
||||
return {}
|
||||
return _merged_env_cache or {}
|
||||
try:
|
||||
if hasattr(db, "get_merged_env_snapshot"):
|
||||
merged = db.get_merged_env_snapshot()
|
||||
@@ -80,10 +93,11 @@ def get_merged_env_dict() -> dict:
|
||||
row = db.get_latest_env()
|
||||
merged = dict((row or {}).get("snapshot") or {})
|
||||
_merged_env_cache = dict(merged or {})
|
||||
_merged_env_cache_ts = now
|
||||
return _merged_env_cache
|
||||
except Exception as e:
|
||||
logger.debug("merged env 조회 실패: %s", e)
|
||||
return {}
|
||||
return _merged_env_cache or {}
|
||||
|
||||
|
||||
def get_strategy_env_dict(strategy_id: str) -> dict:
|
||||
|
||||
232
kis_trader/utils/kr_trading_day.py
Normal file
232
kis_trader/utils/kr_trading_day.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
kis_trader/utils/kr_trading_day.py — 한국 거래일(주말·휴장) 보정 공통
|
||||
====================================================================
|
||||
백테 웹 / CLI / API 날짜 기본값: 주말·휴장이면 **이전 장운영일**로 맞춤.
|
||||
|
||||
휴장일 목록: env ``KR_MARKET_HOLIDAYS`` (콤마구분 YYYY-MM-DD).
|
||||
DB에 없으면 기본 CSV를 env_config_ext 에 1회 시드.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import List, Optional, Set, Tuple, Union
|
||||
|
||||
from kis_trader.utils.env import get_env_from_db, get_env_int
|
||||
|
||||
DateLike = Union[str, date, datetime]
|
||||
|
||||
# 한국거래소 휴장(주말 제외). DB/ env 로 갱신. get_env 기본값용.
|
||||
_DEFAULT_KR_MARKET_HOLIDAYS = (
|
||||
# 2025
|
||||
"2025-01-01,2025-01-28,2025-01-29,2025-01-30,"
|
||||
"2025-03-01,2025-05-05,2025-05-06,2025-06-06,"
|
||||
"2025-08-15,2025-10-03,2025-10-06,2025-10-07,2025-10-08,2025-10-09,2025-12-25,"
|
||||
# 2026
|
||||
"2026-01-01,2026-02-16,2026-02-17,2026-02-18,"
|
||||
"2026-03-01,2026-05-05,2026-05-24,2026-06-06,"
|
||||
"2026-08-15,2026-09-24,2026-09-25,2026-09-26,"
|
||||
"2026-10-03,2026-10-09,2026-12-25,"
|
||||
# 2027 (주요)
|
||||
"2027-01-01,2027-02-08,2027-02-09,2027-02-10,"
|
||||
"2027-03-01,2027-05-05,2027-05-13,2027-06-06,"
|
||||
"2027-08-15,2027-09-14,2027-09-15,2027-09-16,"
|
||||
"2027-10-03,2027-10-09,2027-12-25"
|
||||
)
|
||||
|
||||
_holidays_cache: Optional[Set[date]] = None
|
||||
_holidays_cache_raw: Optional[str] = None
|
||||
_seeded: bool = False
|
||||
|
||||
|
||||
def _parse_ymd(raw: DateLike) -> date:
|
||||
if isinstance(raw, datetime):
|
||||
return raw.date()
|
||||
if isinstance(raw, date):
|
||||
return raw
|
||||
s = str(raw or "").strip()
|
||||
if not s:
|
||||
raise ValueError("empty date")
|
||||
if "-" in s and len(s) >= 10:
|
||||
return datetime.strptime(s[:10], "%Y-%m-%d").date()
|
||||
digits = "".join(c for c in s if c.isdigit())
|
||||
if len(digits) >= 8:
|
||||
return datetime.strptime(digits[:8], "%Y%m%d").date()
|
||||
raise ValueError(f"bad date: {raw!r}")
|
||||
|
||||
|
||||
def ymd(d: date) -> str:
|
||||
return d.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def parse_holiday_csv(raw: str) -> Set[date]:
|
||||
out: Set[date] = set()
|
||||
for part in str(raw or "").replace(";", ",").split(","):
|
||||
p = part.strip()
|
||||
if not p or p.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
out.add(_parse_ymd(p))
|
||||
except ValueError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def ensure_kr_market_holidays_in_db() -> None:
|
||||
"""KR_MARKET_HOLIDAYS / lookback 기본값을 DB/ext 에 없으면 1회 시드 (기존 값 유지)."""
|
||||
global _seeded
|
||||
if _seeded:
|
||||
return
|
||||
_seeded = True
|
||||
try:
|
||||
from kis_trader.utils.env import _get_db # noqa: PLC2701
|
||||
|
||||
db = _get_db()
|
||||
if not db:
|
||||
return
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _seed_if_missing(key: str, value: str) -> None:
|
||||
cur = str(get_env_from_db(key, "") or "").strip()
|
||||
if cur:
|
||||
return
|
||||
row = db.conn.execute(
|
||||
"SELECT env_value FROM env_config_ext WHERE env_key=%s LIMIT 1",
|
||||
(key,),
|
||||
).fetchone()
|
||||
if row and str((row["env_value"] if isinstance(row, dict) else row[0]) or "").strip():
|
||||
return
|
||||
db.conn.execute(
|
||||
"INSERT INTO env_config_ext (env_key, env_value, updated_at) "
|
||||
"VALUES (%s, %s, %s) "
|
||||
"ON DUPLICATE KEY UPDATE env_key=env_key",
|
||||
(key, value, now),
|
||||
)
|
||||
|
||||
_seed_if_missing("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS)
|
||||
_seed_if_missing("PARAM_SEARCH_DEFAULT_LOOKBACK_DAYS", "7")
|
||||
_seed_if_missing("KR_TRADING_DAY_MAX_BACK_DAYS", "14")
|
||||
try:
|
||||
from kis_trader.utils.env import invalidate_merged_env_cache
|
||||
|
||||
invalidate_merged_env_cache()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_kr_market_holiday_set(*, refresh: bool = False) -> Set[date]:
|
||||
"""휴장일 set. env/DB → 없으면 코드 기본 CSV."""
|
||||
global _holidays_cache, _holidays_cache_raw
|
||||
ensure_kr_market_holidays_in_db()
|
||||
raw = str(
|
||||
get_env_from_db("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS) or ""
|
||||
).strip()
|
||||
if not raw:
|
||||
raw = _DEFAULT_KR_MARKET_HOLIDAYS
|
||||
if (
|
||||
not refresh
|
||||
and _holidays_cache is not None
|
||||
and _holidays_cache_raw == raw
|
||||
):
|
||||
return _holidays_cache
|
||||
_holidays_cache = parse_holiday_csv(raw)
|
||||
_holidays_cache_raw = raw
|
||||
return _holidays_cache
|
||||
|
||||
|
||||
def is_kr_trading_day(d: DateLike, *, holidays: Optional[Set[date]] = None) -> bool:
|
||||
"""월~금 이고 휴장 목록에 없으면 True."""
|
||||
day = _parse_ymd(d)
|
||||
if day.weekday() >= 5:
|
||||
return False
|
||||
hol = holidays if holidays is not None else get_kr_market_holiday_set()
|
||||
return day not in hol
|
||||
|
||||
|
||||
def clamp_to_prev_kr_trading_day(
|
||||
d: DateLike,
|
||||
*,
|
||||
holidays: Optional[Set[date]] = None,
|
||||
max_back_days: Optional[int] = None,
|
||||
) -> str:
|
||||
"""
|
||||
거래일이 아니면 하루씩 과거로 이동해 **가장 가까운 이전(또는 당일) 장운영일** 반환.
|
||||
"""
|
||||
day = _parse_ymd(d)
|
||||
hol = holidays if holidays is not None else get_kr_market_holiday_set()
|
||||
limit = int(
|
||||
max_back_days
|
||||
if max_back_days is not None
|
||||
else get_env_int("KR_TRADING_DAY_MAX_BACK_DAYS", 14)
|
||||
)
|
||||
limit = max(1, limit)
|
||||
for _ in range(limit + 1):
|
||||
if is_kr_trading_day(day, holidays=hol):
|
||||
return ymd(day)
|
||||
day -= timedelta(days=1)
|
||||
return ymd(day)
|
||||
|
||||
|
||||
def default_kr_trading_end(*, as_of: Optional[DateLike] = None) -> str:
|
||||
"""종료일 기본값 = as_of(기본 오늘) 을 이전 거래일로 보정."""
|
||||
base = _parse_ymd(as_of) if as_of is not None else date.today()
|
||||
return clamp_to_prev_kr_trading_day(base)
|
||||
|
||||
|
||||
def default_kr_trading_start(
|
||||
lookback_days: int = 7,
|
||||
*,
|
||||
as_of: Optional[DateLike] = None,
|
||||
end: Optional[DateLike] = None,
|
||||
) -> str:
|
||||
"""시작일 기본값 = (종료일 − lookback) 을 이전 거래일로 보정."""
|
||||
end_d = _parse_ymd(end) if end is not None else _parse_ymd(
|
||||
default_kr_trading_end(as_of=as_of)
|
||||
)
|
||||
lb = max(1, int(lookback_days))
|
||||
start_raw = end_d - timedelta(days=lb)
|
||||
return clamp_to_prev_kr_trading_day(start_raw)
|
||||
|
||||
|
||||
def resolve_kr_backtest_date_defaults(
|
||||
lookback_days: int = 7,
|
||||
*,
|
||||
as_of: Optional[DateLike] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""(start, end) 기본 구간. end·start 모두 거래일."""
|
||||
end = default_kr_trading_end(as_of=as_of)
|
||||
start = default_kr_trading_start(lookback_days, end=end)
|
||||
if start > end:
|
||||
start = end
|
||||
return start, end
|
||||
|
||||
|
||||
def clamp_to_prev_weekday(d: DateLike, *, max_back_days: int = 14) -> str:
|
||||
"""주말만 보정 (해외장 등 공휴일 목록 없이 토·일만 스킵)."""
|
||||
day = _parse_ymd(d)
|
||||
for _ in range(max(1, int(max_back_days)) + 1):
|
||||
if day.weekday() < 5:
|
||||
return ymd(day)
|
||||
day -= timedelta(days=1)
|
||||
return ymd(day)
|
||||
|
||||
|
||||
def holiday_list_ymd() -> List[str]:
|
||||
return sorted(ymd(d) for d in get_kr_market_holiday_set())
|
||||
|
||||
|
||||
def trading_dates_payload(lookback_days: int = 7) -> dict:
|
||||
"""웹/API용 날짜 기본값 묶음."""
|
||||
start, end = resolve_kr_backtest_date_defaults(lookback_days)
|
||||
today = ymd(date.today())
|
||||
return {
|
||||
"today": today,
|
||||
"end": end,
|
||||
"start": start,
|
||||
"trading_day": end, # 단일일 조회용 (= 보정된 종료일)
|
||||
"holidays": holiday_list_ymd(),
|
||||
"lookback_days": int(lookback_days),
|
||||
"is_today_trading_day": is_kr_trading_day(today),
|
||||
}
|
||||
142
kis_trader/utils/non_stock.py
Normal file
142
kis_trader/utils/non_stock.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
비본주(우선주·ETF·ETN·스팩) 판별 — 후보 매수 차단 공용.
|
||||
|
||||
키움 조건검색은 종목명 없이 code 만 주는 경우가 많아(name==code),
|
||||
이름 끝 '우' 만으로는 우선주를 못 막는다. 코드 끝자리 규칙으로 보완한다.
|
||||
|
||||
KRX 단축코드 끝자리(종목구분):
|
||||
- 보통주: 대개 '0'
|
||||
- 우선주: '5','7','9' (구형) + 'K','L','M'… (2013년 이후 알파벳)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import FrozenSet
|
||||
|
||||
from .env import get_env_bool, get_env_from_db
|
||||
|
||||
# ETF/ETN 브랜드·유형 (이름 부분일치). '파워' 단독은 뉴파워프라즈마 오탐 → 넣지 않음.
|
||||
_DEFAULT_NON_STOCK_NAME_KEYWORDS = (
|
||||
"ETN",
|
||||
"ETF",
|
||||
"레버리지",
|
||||
"인버스",
|
||||
"2X",
|
||||
"3X",
|
||||
"선물",
|
||||
"KODEX",
|
||||
"TIGER",
|
||||
"KBSTAR",
|
||||
"ARIRANG",
|
||||
"HANARO",
|
||||
"SOL ",
|
||||
"KOSEF",
|
||||
"ACE ",
|
||||
"KINDEX",
|
||||
"RISE ",
|
||||
"PLUS ",
|
||||
"TIMEFOLIO",
|
||||
"히어로",
|
||||
"SMART",
|
||||
"TREX",
|
||||
"WON",
|
||||
"KOACT",
|
||||
)
|
||||
|
||||
|
||||
def _pref_digit_suffixes() -> FrozenSet[str]:
|
||||
"""우선주 숫자 끝자리 — NON_STOCK_PREF_CODE_SUFFIXES (기본 5,7,9)."""
|
||||
raw = get_env_from_db("NON_STOCK_PREF_CODE_SUFFIXES", "5,7,9") or "5,7,9"
|
||||
return frozenset(s.strip().upper() for s in str(raw).split(",") if s.strip())
|
||||
|
||||
|
||||
def is_preferred_code(code: str) -> bool:
|
||||
"""종목코드만으로 우선주 여부 (name 없어도 판단)."""
|
||||
c = (code or "").strip()
|
||||
if len(c) < 1:
|
||||
return False
|
||||
last = c[-1].upper()
|
||||
if last in _pref_digit_suffixes():
|
||||
return True
|
||||
# K,L,M… 알파벳 끝 = 신형 우선주 코드
|
||||
if get_env_bool("NON_STOCK_EXCLUDE_ALPHA_SUFFIX", True) and last.isalpha():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_etn_code(code: str) -> bool:
|
||||
c = (code or "").strip()
|
||||
return len(c) == 6 and c[0] == "7"
|
||||
|
||||
|
||||
def _name_keywords() -> tuple:
|
||||
"""NON_STOCK_NAME_KEYWORDS — 콤마 구분. 비우면 기본 튜플."""
|
||||
raw = (get_env_from_db("NON_STOCK_NAME_KEYWORDS", "") or "").strip()
|
||||
if not raw:
|
||||
return _DEFAULT_NON_STOCK_NAME_KEYWORDS
|
||||
parts = tuple(s.strip() for s in raw.split(",") if s.strip())
|
||||
return parts or _DEFAULT_NON_STOCK_NAME_KEYWORDS
|
||||
|
||||
|
||||
def _name_hits_keyword(name: str) -> bool:
|
||||
nm = name.upper()
|
||||
for k in _name_keywords():
|
||||
if not k:
|
||||
continue
|
||||
if k.isascii():
|
||||
if k.upper() in nm:
|
||||
return True
|
||||
elif k in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_non_stock(name: str, code: str) -> bool:
|
||||
"""ETN/ETF/스팩/우선주 등 매수 금지 종목이면 True.
|
||||
|
||||
name 이 비었거나 name==code 여도 코드 규칙으로 우선주·ETN 을 잡는다.
|
||||
"""
|
||||
code = (code or "").strip()
|
||||
name = (name or "").strip()
|
||||
|
||||
if not code:
|
||||
return True
|
||||
|
||||
if is_preferred_code(code):
|
||||
return True
|
||||
if is_etn_code(code):
|
||||
return True
|
||||
|
||||
if name:
|
||||
if _name_hits_keyword(name):
|
||||
return True
|
||||
nm = name.upper()
|
||||
if "스팩" in name or "SPAC" in nm:
|
||||
return True
|
||||
# 우선주 표기 (우 / 우B / 우C …)
|
||||
if name.endswith("우") or "우B" in name or "우C" in name or name.endswith("(전환)"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_unmanageable_holding(name: str, code: str) -> bool:
|
||||
"""보유 동기화에서 빼서 봇이 매도하지 않을 종목 (시세 불가 ETF/ETN·스팩).
|
||||
|
||||
우선주는 시세·매도 가능 → False. (실수로 산 우선주는 매도로 정리)
|
||||
"""
|
||||
code = (code or "").strip()
|
||||
name = (name or "").strip()
|
||||
if is_etn_code(code):
|
||||
return True
|
||||
if not name or name == code:
|
||||
return False
|
||||
nm = name.upper()
|
||||
for k in ("ETN", "ETF", "레버리지", "인버스", "KODEX", "TIGER", "KBSTAR"):
|
||||
if k.isascii():
|
||||
if k in nm:
|
||||
return True
|
||||
elif k in name:
|
||||
return True
|
||||
if "스팩" in name or "SPAC" in nm:
|
||||
return True
|
||||
return False
|
||||
97
kis_trader/utils/stock_name.py
Normal file
97
kis_trader/utils/stock_name.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
종목코드 → 표시용 종목명 (Mattermost·로그·DB 공통).
|
||||
|
||||
키움 조건검색 실시간 응답에는 종목명이 없어 code=이름 임시값이 들어오는 경우가 많다.
|
||||
우선순위: 유효 fallback → 메모리 캐시 → stock_meta → target_candidates_history → 잔고 prdt_name
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 프로세스 내 재조회 방지 (매수체크 루프·체결 알림 공용)
|
||||
_NAME_CACHE: Dict[str, str] = {}
|
||||
|
||||
|
||||
def resolve_stock_display_name(
|
||||
db,
|
||||
code: str,
|
||||
fallback: str = "",
|
||||
*,
|
||||
holdings_map: Optional[Dict[str, Dict]] = None,
|
||||
cache_to_meta: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
종목코드에 대응하는 한글 종목명을 반환한다.
|
||||
|
||||
Args:
|
||||
db: TradeDB / TradeDBExt 인스턴스
|
||||
code: 6자리 종목코드
|
||||
fallback: 전략·후보에서 넘어온 이름 (코드와 같으면 무시)
|
||||
holdings_map: {code: {name, qty, ...}} — OrderManager 잔고 캐시 등
|
||||
cache_to_meta: history/잔고에서 찾은 이름을 stock_meta 에 저장
|
||||
"""
|
||||
code = str(code or "").strip()
|
||||
if not code:
|
||||
return str(fallback or "").strip()
|
||||
|
||||
fb = str(fallback or "").strip()
|
||||
if fb and fb != code:
|
||||
_NAME_CACHE[code] = fb
|
||||
return fb
|
||||
|
||||
cached = _NAME_CACHE.get(code)
|
||||
if cached and cached != code:
|
||||
return cached
|
||||
|
||||
# 1) stock_meta
|
||||
try:
|
||||
if hasattr(db, "get_stock_meta"):
|
||||
meta = db.get_stock_meta(code)
|
||||
if meta:
|
||||
n = str(meta.get("name") or "").strip()
|
||||
if n and n != code:
|
||||
_NAME_CACHE[code] = n
|
||||
return n
|
||||
except Exception as exc:
|
||||
logger.debug("get_stock_meta 실패(%s): %s", code, exc)
|
||||
|
||||
# 2) target_candidates_history (조건검색 스냅샷 — 실매 후보 적재)
|
||||
try:
|
||||
conn = getattr(db, "conn", None)
|
||||
if conn is not None:
|
||||
row = conn.execute(
|
||||
"SELECT name FROM target_candidates_history "
|
||||
"WHERE code = %s AND name IS NOT NULL AND name != '' AND name != code "
|
||||
"ORDER BY COALESCE(event_time, scan_time) DESC, id DESC LIMIT 1",
|
||||
(code,),
|
||||
).fetchone()
|
||||
if row:
|
||||
n = str(row.get("name") or "").strip()
|
||||
if n and n != code:
|
||||
_NAME_CACHE[code] = n
|
||||
if cache_to_meta and hasattr(db, "upsert_stock_meta"):
|
||||
try:
|
||||
db.upsert_stock_meta(code, name=n)
|
||||
except Exception:
|
||||
pass
|
||||
return n
|
||||
except Exception as exc:
|
||||
logger.debug("target_candidates_history 이름 조회 실패(%s): %s", code, exc)
|
||||
|
||||
# 3) 실계좌 잔고 (보유 종목 prdt_name)
|
||||
if holdings_map:
|
||||
h = holdings_map.get(code) or {}
|
||||
n = str(h.get("name") or "").strip()
|
||||
if n and n != code:
|
||||
_NAME_CACHE[code] = n
|
||||
if cache_to_meta and hasattr(db, "upsert_stock_meta"):
|
||||
try:
|
||||
db.upsert_stock_meta(code, name=n)
|
||||
except Exception:
|
||||
pass
|
||||
return n
|
||||
|
||||
return fb if fb else code
|
||||
@@ -23,11 +23,10 @@ KIS_TRADER_STRATEGY_IDS: List[str] = [
|
||||
|
||||
# 사용자 지정 — 웹·계산에서 완전히 숨길 전략 (비활성·중복·저성능).
|
||||
# DB 에 기존 거래기록이 남아 있어도 실거래 분석·대시보드·보유탭·운영설정에서 표시·집계하지 않는다.
|
||||
# SCALP : MOMENTUM 과 1분봉 슬롯 중복 (반등 vs 추세) → MOMENTUM 만 사용
|
||||
# RANGE_BREAK : BREAKOUT 과 돌파 컨셉 중복 + 미검증 (파라서치 거래 0건)
|
||||
# DBBAND : 표본 부족·미성숙 (백테 2~4건)
|
||||
# ※ SCALP 는 실거래 분석·대시보드에 노출 (스캘핑 봇 운영 중)
|
||||
HIDDEN_STRATEGY_IDS: List[str] = [
|
||||
"SCALP",
|
||||
"RANGE_BREAK",
|
||||
"DBBAND",
|
||||
]
|
||||
|
||||
32
kis_trader/utils/trade_time.py
Normal file
32
kis_trader/utils/trade_time.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
kis_trader/utils/trade_time.py — 실매·백테 시각 파싱 공통
|
||||
========================================================
|
||||
실매 buy_time: ``YYYY-MM-DD HH:MM:SS``
|
||||
백테 봉/틱키: ``YYYYMMDDHHMM`` / ``YYYYMMDDHHMMSS``
|
||||
|
||||
엔진별 ``_t2dt`` 가 ``[:12]``+``%Y%m%d%H%M`` 만 쓰면 실매 시간컷이
|
||||
``None`` 으로 무력화된다 → 전 전략이 이 함수를 쓰도록 통일.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def parse_trade_datetime(t: str) -> datetime:
|
||||
"""실매·백테 시각 문자열 → datetime. 실패 시 ValueError."""
|
||||
s = str(t or "").strip()
|
||||
if not s:
|
||||
raise ValueError("empty time")
|
||||
if "-" in s or "T" in s or ":" in s:
|
||||
compact = (
|
||||
s.replace("-", "").replace(":", "").replace(" ", "").replace("T", "")
|
||||
)
|
||||
digits = "".join(c for c in compact if c.isdigit())
|
||||
else:
|
||||
digits = "".join(c for c in s if c.isdigit())
|
||||
if len(digits) >= 14:
|
||||
return datetime.strptime(digits[:14], "%Y%m%d%H%M%S")
|
||||
if len(digits) >= 12:
|
||||
return datetime.strptime(digits[:12], "%Y%m%d%H%M")
|
||||
raise ValueError(f"bad time: {t!r}")
|
||||
Reference in New Issue
Block a user