feat: Enhance trading system with new e_min_chg_pct parameter and related logic
Changes: - Introduced the `e_min_chg_pct` parameter to define the minimum price change percentage compared to the previous day's close, enhancing the momentum trading strategy. - Updated various functions and classes to incorporate this new parameter, ensuring it is utilized in both backtesting and live trading scenarios. - Improved documentation and comments to clarify the purpose and usage of the new parameter across the codebase. Impact: - This addition allows for more precise control over trading conditions, potentially increasing the effectiveness of the momentum strategy while maintaining system integrity and performance.
This commit is contained in:
454
scripts/query_kiwoom_condition_snapshot.py
Normal file
454
scripts/query_kiwoom_condition_snapshot.py
Normal file
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
키움 저장조건식 → 현재 매칭 종목 1회 조회 (HTS 실검 대조용)
|
||||
============================================================
|
||||
|
||||
실매 봇 RAM/히스토리 sticky 가 아니라, 키움 WS 로 CNSRREQ 초기 응답을
|
||||
받아 **지금 서버 조건식이 뽑는 종목**을 출력한다.
|
||||
|
||||
흐름:
|
||||
LOGIN → CNSRLST → (이름/seq로) CNSRREQ → 초기 data 출력 → CNSRCLR → 종료
|
||||
|
||||
주의:
|
||||
- 실매 ``kis_trader_main`` 과 **같은 키움 계정** 을 쓴다.
|
||||
이미 실시간 등록된 seq 면 900003 이 날 수 있어, 1회 CLR→REQ 후 다시 CLR 한다.
|
||||
- 장후에는 편입/이탈 push 가 거의 없고, 초기 리스트만 의미 있다.
|
||||
- 주문·조건식 저장은 하지 않는다.
|
||||
|
||||
사용:
|
||||
cd /home/hoon/kis_bot
|
||||
python3 -u scripts/query_kiwoom_condition_snapshot.py
|
||||
python3 -u scripts/query_kiwoom_condition_snapshot.py --name momentum
|
||||
python3 -u scripts/query_kiwoom_condition_snapshot.py --seq 3
|
||||
python3 -u scripts/query_kiwoom_condition_snapshot.py --name momentum --compare-db
|
||||
|
||||
로그:
|
||||
logs/kiwoom_cond_snapshot_YYYYMMDD_HHMMSS.log
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from kis_trader.utils.env import get_env_from_db # noqa: E402
|
||||
from kis_trader.ws.kis_ws import _get_kiwoom_token_cached # noqa: E402
|
||||
|
||||
|
||||
def _load_kiwoom_creds() -> Tuple[str, str, bool]:
|
||||
kw_mock_raw = (get_env_from_db("KIWOOM_MOCK", "") or "").strip().lower()
|
||||
is_mock = kw_mock_raw in ("1", "true", "y", "yes", "on")
|
||||
if is_mock:
|
||||
key = (get_env_from_db("KIWOOM_APP_KEY_MOCK", "") or "").strip()
|
||||
secret = (get_env_from_db("KIWOOM_APP_SECRET_MOCK", "") or "").strip()
|
||||
else:
|
||||
key = (get_env_from_db("KIWOOM_APP_KEY_REAL", "") or "").strip()
|
||||
secret = (get_env_from_db("KIWOOM_APP_SECRET_REAL", "") or "").strip()
|
||||
if not key or not secret:
|
||||
key = key or (get_env_from_db("KIWOOM_APP_KEY", "") or "").strip()
|
||||
secret = secret or (get_env_from_db("KIWOOM_APP_SECRET", "") or "").strip()
|
||||
return key, secret, is_mock
|
||||
|
||||
|
||||
def _seq_name(item: Any) -> Tuple[str, str]:
|
||||
if isinstance(item, (list, tuple)):
|
||||
return (
|
||||
str(item[0]) if len(item) > 0 else "",
|
||||
str(item[1]) if len(item) > 1 else "",
|
||||
)
|
||||
if isinstance(item, dict):
|
||||
return str(item.get("seq") or ""), str(item.get("name") or "")
|
||||
return "", ""
|
||||
|
||||
|
||||
def _extract_code(item: Any) -> str:
|
||||
raw = ""
|
||||
if isinstance(item, dict):
|
||||
raw = str(item.get("9001") or item.get("jmcode") or item.get("code") or "").strip()
|
||||
elif isinstance(item, (list, tuple)) and item:
|
||||
raw = str(item[0]).strip()
|
||||
if not raw:
|
||||
return ""
|
||||
# 키움 조건검색 응답은 종종 'A005930' 형태 — 비교·표시용으로 A 접두 제거
|
||||
if len(raw) >= 7 and raw[0] in ("A", "a") and raw[1:].isdigit():
|
||||
return raw[1:]
|
||||
if raw.upper().startswith("A") and len(raw) == 7:
|
||||
return raw[1:]
|
||||
return raw
|
||||
|
||||
|
||||
def _resolve_names(codes: List[str]) -> Dict[str, str]:
|
||||
"""DB 히스토리에서 종목명 보강 (없으면 코드 그대로)."""
|
||||
out: Dict[str, str] = {c: c for c in codes}
|
||||
if not codes:
|
||||
return out
|
||||
try:
|
||||
from database import TradeDB
|
||||
|
||||
db = TradeDB()
|
||||
try:
|
||||
for c in codes:
|
||||
row = db.conn.execute(
|
||||
"SELECT name FROM target_candidates_history "
|
||||
"WHERE code=%s AND name IS NOT NULL AND name<>'' AND name<>code "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(c,),
|
||||
).fetchone()
|
||||
if row and row.get("name"):
|
||||
out[c] = str(row["name"])
|
||||
continue
|
||||
row2 = db.conn.execute(
|
||||
"SELECT name FROM ls_candidates_history "
|
||||
"WHERE code=%s AND name IS NOT NULL AND name<>'' AND name<>code "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(c,),
|
||||
).fetchone()
|
||||
if row2 and row2.get("name"):
|
||||
out[c] = str(row2["name"])
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 종목명 DB 보강 스킵: {e}")
|
||||
return out
|
||||
|
||||
|
||||
def _latest_db_universe(strategy_id: str = "MOMENTUM") -> Tuple[str, List[str]]:
|
||||
from database import TradeDB
|
||||
|
||||
db = TradeDB()
|
||||
try:
|
||||
slot = db.conn.execute(
|
||||
"SELECT slot_key FROM target_candidates_history "
|
||||
"WHERE strategy_id=%s GROUP BY slot_key ORDER BY slot_key DESC LIMIT 1",
|
||||
(strategy_id,),
|
||||
).fetchone()
|
||||
if not slot:
|
||||
return "", []
|
||||
sk = str(slot["slot_key"])
|
||||
rows = db.conn.execute(
|
||||
"SELECT code FROM target_candidates_history "
|
||||
"WHERE strategy_id=%s AND slot_key=%s ORDER BY code",
|
||||
(strategy_id, sk),
|
||||
).fetchall()
|
||||
return sk, [str(r["code"]) for r in rows]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def query_snapshot(
|
||||
*,
|
||||
name: str = "momentum",
|
||||
seq: str = "",
|
||||
search_type: str = "1",
|
||||
timeout_sec: float = 25.0,
|
||||
log_path: Optional[Path] = None,
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
import websocket
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"websocket-client 미설치: {e}") from e
|
||||
|
||||
key, secret, is_mock = _load_kiwoom_creds()
|
||||
if not key or not secret:
|
||||
raise RuntimeError("키움 앱키/시크릿 미설정 (KIWOOM_APP_KEY_REAL 등)")
|
||||
|
||||
token = _get_kiwoom_token_cached(key, secret, is_mock)
|
||||
if not token:
|
||||
raise RuntimeError("키움 토큰 발급 실패 (au10001)")
|
||||
|
||||
url = (
|
||||
get_env_from_db("KIWOOM_WS_URL_MOCK", "wss://mockapi.kiwoom.com:10000/api/dostk/websocket")
|
||||
if is_mock
|
||||
else get_env_from_db("KIWOOM_WS_URL_REAL", "wss://api.kiwoom.com:10000/api/dostk/websocket")
|
||||
)
|
||||
|
||||
st: Dict[str, Any] = {
|
||||
"seq": (seq or "").strip(),
|
||||
"name": (name or "").strip(),
|
||||
"conditions": [],
|
||||
"codes": [],
|
||||
"error": None,
|
||||
"done": False,
|
||||
"cleared": False,
|
||||
"retried_900003": False,
|
||||
"raw_cnsrreq": None,
|
||||
"mock": is_mock,
|
||||
"url": url,
|
||||
}
|
||||
lock = threading.Lock()
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
line = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"
|
||||
print(line, flush=True)
|
||||
if log_path:
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
def _send(ws, payload: dict) -> None:
|
||||
ws.send(json.dumps(payload))
|
||||
|
||||
def on_open(ws):
|
||||
_log("LOGIN 발송")
|
||||
_send(ws, {"trnm": "LOGIN", "token": token})
|
||||
|
||||
def on_message(ws, message):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except Exception:
|
||||
_log(f"non-json: {str(message)[:200]}")
|
||||
return
|
||||
trnm = data.get("trnm")
|
||||
if trnm == "PING":
|
||||
try:
|
||||
ws.send(message)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
if trnm == "LOGIN":
|
||||
rc = str(data.get("return_code"))
|
||||
if rc not in ("0", "0.0"):
|
||||
st["error"] = f"LOGIN 실패 rc={rc} msg={data.get('return_msg')}"
|
||||
ws.close()
|
||||
return
|
||||
_log("LOGIN OK → CNSRLST")
|
||||
_send(ws, {"trnm": "CNSRLST"})
|
||||
return
|
||||
|
||||
if trnm == "CNSRLST":
|
||||
items = data.get("data") or []
|
||||
st["conditions"] = [
|
||||
{"seq": a, "name": b} for a, b in (_seq_name(it) for it in items) if a or b
|
||||
]
|
||||
_log(
|
||||
"저장조건식 %d개: %s"
|
||||
% (
|
||||
len(st["conditions"]),
|
||||
", ".join(f"{c['seq']}:{c['name']}" for c in st["conditions"]),
|
||||
)
|
||||
)
|
||||
target_seq = st["seq"]
|
||||
if not target_seq:
|
||||
want = st["name"].lower()
|
||||
for c in st["conditions"]:
|
||||
if str(c["name"]).strip().lower() == want:
|
||||
target_seq = str(c["seq"])
|
||||
break
|
||||
if not target_seq:
|
||||
st["error"] = f"조건식 없음 name={st['name']!r} seq={st['seq']!r}"
|
||||
ws.close()
|
||||
return
|
||||
st["seq"] = target_seq
|
||||
_log(
|
||||
f"CNSRREQ seq={target_seq} name={st['name'] or '?'} "
|
||||
f"search_type={search_type}"
|
||||
)
|
||||
_send(
|
||||
ws,
|
||||
{
|
||||
"trnm": "CNSRREQ",
|
||||
"seq": target_seq,
|
||||
"search_type": str(search_type),
|
||||
"stex_tp": "K",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if trnm == "CNSRREQ":
|
||||
st["raw_cnsrreq"] = data
|
||||
rc = str(data.get("return_code"))
|
||||
msg = str(data.get("return_msg") or "")
|
||||
if rc == "900003" and not st["retried_900003"]:
|
||||
st["retried_900003"] = True
|
||||
_log(f"900003 이미등록 → CNSRCLR 1회 후 재요청 seq={st['seq']}")
|
||||
_send(ws, {"trnm": "CNSRCLR", "seq": st["seq"]})
|
||||
return
|
||||
if rc not in ("0", "0.0"):
|
||||
st["error"] = f"CNSRREQ 실패 rc={rc} msg={msg}"
|
||||
try:
|
||||
_send(ws, {"trnm": "CNSRCLR", "seq": st["seq"]})
|
||||
except Exception:
|
||||
pass
|
||||
ws.close()
|
||||
return
|
||||
codes = [_extract_code(it) for it in (data.get("data") or [])]
|
||||
codes = sorted({c for c in codes if c})
|
||||
st["codes"] = codes
|
||||
_log(f"CNSRREQ OK — 매칭 {len(codes)}종목")
|
||||
_send(ws, {"trnm": "CNSRCLR", "seq": st["seq"]})
|
||||
return
|
||||
|
||||
if trnm == "CNSRCLR":
|
||||
rc = str(data.get("return_code"))
|
||||
_log(f"CNSRCLR rc={rc}")
|
||||
# 900003 재시도 경로: CLR 후 다시 REQ
|
||||
if st["retried_900003"] and not st["codes"] and not st.get("_req_after_clr"):
|
||||
st["_req_after_clr"] = True
|
||||
time.sleep(0.8)
|
||||
_log(f"CLR 후 CNSRREQ 재발송 seq={st['seq']}")
|
||||
_send(
|
||||
ws,
|
||||
{
|
||||
"trnm": "CNSRREQ",
|
||||
"seq": st["seq"],
|
||||
"search_type": str(search_type),
|
||||
"stex_tp": "K",
|
||||
},
|
||||
)
|
||||
return
|
||||
st["cleared"] = True
|
||||
st["done"] = True
|
||||
ws.close()
|
||||
return
|
||||
|
||||
_log(f"기타 {trnm}: {json.dumps(data, ensure_ascii=False)[:240]}")
|
||||
|
||||
def on_error(ws, err):
|
||||
with lock:
|
||||
if not st["error"]:
|
||||
st["error"] = f"WS 오류: {err}"
|
||||
_log(f"WS error: {err}")
|
||||
|
||||
def on_close(ws, code, msg):
|
||||
st["done"] = True
|
||||
|
||||
ws = websocket.WebSocketApp(
|
||||
url,
|
||||
on_open=on_open,
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close,
|
||||
)
|
||||
th = threading.Thread(
|
||||
target=ws.run_forever,
|
||||
kwargs={"ping_interval": 0},
|
||||
daemon=True,
|
||||
)
|
||||
th.start()
|
||||
|
||||
deadline = time.time() + float(timeout_sec)
|
||||
while time.time() < deadline and not st["done"]:
|
||||
if st["error"] and not st.get("_req_after_clr"):
|
||||
# 에러 후 CLR 대기 중일 수 있음
|
||||
if "CNSRREQ 실패" in str(st["error"]):
|
||||
time.sleep(0.5)
|
||||
break
|
||||
time.sleep(0.15)
|
||||
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
th.join(timeout=3.0)
|
||||
|
||||
if st["error"] and not st["codes"]:
|
||||
raise RuntimeError(st["error"])
|
||||
return st
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="키움 조건식 현재 매칭 종목 1회 조회")
|
||||
ap.add_argument("--name", default="momentum", help="조건식 이름 (기본 momentum)")
|
||||
ap.add_argument("--seq", default="", help="seq 직접 지정 시 이름 무시")
|
||||
ap.add_argument(
|
||||
"--search-type",
|
||||
default="1",
|
||||
help="CNSRREQ search_type (봇과 동일 기본 1)",
|
||||
)
|
||||
ap.add_argument("--timeout", type=float, default=25.0)
|
||||
ap.add_argument(
|
||||
"--compare-db",
|
||||
action="store_true",
|
||||
help="target_candidates_history 최신 MOMENTUM 슬롯과 비교",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--strategy-id",
|
||||
default="MOMENTUM",
|
||||
help="--compare-db 시 history strategy_id",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
log_dir = ROOT / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_path = log_dir / f"kiwoom_cond_snapshot_{ts}.log"
|
||||
|
||||
print(f"📝 log: {log_path}")
|
||||
print(
|
||||
"⚠️ 실매 봇과 동일 키움 계정 — 조회 후 CNSRCLR 함. "
|
||||
"장중엔 봇 조건등록과 순간 충돌 가능."
|
||||
)
|
||||
|
||||
try:
|
||||
st = query_snapshot(
|
||||
name=args.name,
|
||||
seq=args.seq,
|
||||
search_type=args.search_type,
|
||||
timeout_sec=args.timeout,
|
||||
log_path=log_path,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"❌ {e}")
|
||||
return 1
|
||||
|
||||
codes: List[str] = list(st.get("codes") or [])
|
||||
names = _resolve_names(codes)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(
|
||||
f"키움 조건식 스냅샷 name={args.name!r} seq={st.get('seq')} "
|
||||
f"매칭={len(codes)}종 mock={st.get('mock')}"
|
||||
)
|
||||
print("=" * 60)
|
||||
if not codes:
|
||||
print("(매칭 종목 없음)")
|
||||
else:
|
||||
for i, c in enumerate(codes, 1):
|
||||
print(f" {i:2d}. {c} {names.get(c, c)}")
|
||||
print("=" * 60)
|
||||
|
||||
if args.compare_db:
|
||||
sk, db_codes = _latest_db_universe(args.strategy_id)
|
||||
db_set: Set[str] = set(db_codes)
|
||||
live_set: Set[str] = set(codes)
|
||||
only_api = sorted(live_set - db_set)
|
||||
only_db = sorted(db_set - live_set)
|
||||
both = sorted(live_set & db_set)
|
||||
print()
|
||||
print(f"[DB 비교] strategy={args.strategy_id} latest_slot={sk} n={len(db_codes)}")
|
||||
print(f" 교집합 {len(both)}: {both}")
|
||||
print(f" API만 {len(only_api)}: {only_api}")
|
||||
print(f" DB만(sticky) {len(only_db)}: {only_db}")
|
||||
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(
|
||||
json.dumps(
|
||||
{
|
||||
"seq": st.get("seq"),
|
||||
"name": args.name,
|
||||
"codes": codes,
|
||||
"names": names,
|
||||
"conditions": st.get("conditions"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
print(f"✅ 완료 — {log_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -26,6 +26,8 @@ MIN_PF="${MIN_PF:-0}"
|
||||
# STRATEGIES="momentum tail" bash ...
|
||||
# STRATEGIES="us_momentum momentum" bash ...
|
||||
STRATEGIES="${STRATEGIES:-momentum tail breakout scalp}"
|
||||
# kiwoom|ls — 웹 Optuna 이력소스 / CLI UNIVERSE_HISTORY_SOURCE
|
||||
UNIVERSE_HISTORY_SOURCE="${UNIVERSE_HISTORY_SOURCE:-${BACKTEST_UNIVERSE_HISTORY_SOURCE:-kiwoom}}"
|
||||
PY="${PY:-.venv/bin/python}"
|
||||
TS0="$(date +%Y%m%d_%H%M%S)"
|
||||
MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log"
|
||||
@@ -34,6 +36,7 @@ MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log"
|
||||
echo "======== Optuna 4전략 TPE 순차 시작 $(date -Is) ========"
|
||||
echo "START=$START END=$END MODE=$MODE TRIALS=$TRIALS"
|
||||
echo "STRATEGIES=$STRATEGIES"
|
||||
echo "UNIVERSE_HISTORY_SOURCE=$UNIVERSE_HISTORY_SOURCE"
|
||||
echo "min_wr=$MIN_WIN_RATE min_pf=$MIN_PF min_trades=$MIN_TRADES"
|
||||
echo "apply-best=OFF orderbook=off n_jobs=1 (사후 results_gated + briefing.md)"
|
||||
echo "master_log=$MASTER"
|
||||
@@ -55,7 +58,7 @@ run_one() {
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "-------- [$strat] START $(date -Is) study=$study --------"
|
||||
echo "-------- [$strat] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE --------"
|
||||
} | tee -a "$MASTER"
|
||||
echo "$log" > "logs/optuna_${strat}_tpe_latest.logpath"
|
||||
echo "$study" > "logs/optuna_${strat}_tpe_latest.study"
|
||||
@@ -74,6 +77,7 @@ run_one() {
|
||||
--no-progress \
|
||||
--study-name "$study" \
|
||||
--sort-by "$sort_by" \
|
||||
--universe-history-source "$UNIVERSE_HISTORY_SOURCE" \
|
||||
>"$log" 2>&1
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
@@ -170,16 +170,28 @@ def main() -> int:
|
||||
hb = threading.Thread(target=_heartbeat, args=(prog_file, stop_hb), daemon=True)
|
||||
hb.start()
|
||||
_write_progress(prog_file, pct=30, phase="engine", message="웹엔진 실행중")
|
||||
data = None
|
||||
http_status = 200
|
||||
try:
|
||||
with bw.app.test_request_context(f"{path}?{qs}"):
|
||||
resp = fn()
|
||||
if isinstance(resp, tuple):
|
||||
# Flask: (jsonify(...), 500) — 본문만 취하면 실패를 성공으로 오인함
|
||||
http_status = int(resp[1]) if len(resp) > 1 else 200
|
||||
resp = resp[0]
|
||||
elif hasattr(resp, "status_code"):
|
||||
try:
|
||||
http_status = int(resp.status_code)
|
||||
except (TypeError, ValueError):
|
||||
http_status = 200
|
||||
data = resp.get_json(silent=True) if hasattr(resp, "get_json") else None
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(f"응답 JSON 아님: {type(resp)}")
|
||||
if data.get("error") and not data.get("summary") and not data.get("ok", True):
|
||||
raise RuntimeError(str(data.get("error")))
|
||||
# 핸들러가 jsonify({"error": ...}, 500) 을 주면 summary 없이 error 만 옴
|
||||
if http_status >= 400 or (
|
||||
data.get("error") and not data.get("summary")
|
||||
):
|
||||
raise RuntimeError(str(data.get("error") or f"HTTP {http_status}"))
|
||||
finally:
|
||||
stop_hb.set()
|
||||
try:
|
||||
@@ -187,10 +199,14 @@ def main() -> int:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("백테 응답 없음")
|
||||
|
||||
elapsed = time.time() - t0
|
||||
summary = data.get("summary") or {}
|
||||
trades = data.get("trades") or []
|
||||
out = dict(data)
|
||||
out.pop("error", None)
|
||||
out["ok"] = True
|
||||
out["job_id"] = job_id
|
||||
out["kind"] = "strategy_bt_cli"
|
||||
|
||||
Reference in New Issue
Block a user