模拟盘无 API Key 时复盘K线改为优先走 OKX 公开行情接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-09-15 12:16:06 +08:00
parent d6bb99b5bb
commit d16821c7b7
3 changed files with 172 additions and 16 deletions
+119 -15
View File
@@ -924,19 +924,118 @@ def _ohlcv_dict_rows_to_lists(rows, lim):
return [[r["ts"], r["o"], r["h"], r["l"], r["c"], r.get("v", 0)] for r in pick]
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
lim = max(2, int(limit or ORDER_CHART_LIMIT))
def _okx_swap_inst_id(exchange_symbol):
"""ETH/USDT:USDT → ETH-USDT-SWAP(公开 candles 用)."""
sym = str(exchange_symbol or "").strip().upper()
if not sym:
return ""
if "/" in sym:
base, rest = sym.split("/", 1)
quote = rest.split(":", 1)[0].strip() or "USDT"
return f"{base.strip()}-{quote}-SWAP"
if sym.endswith("-SWAP"):
return sym
if "-" in sym:
return sym
if sym.endswith("USDT") and len(sym) > 4:
return f"{sym[:-4]}-USDT-SWAP"
return sym
def _okx_public_bar(timeframe):
"""ccxt 周期 → OKX public bar(1H/4H 大写 H)."""
s = str(timeframe or "").strip()
if not s:
return "15m"
low = s.lower()
if low.endswith("m") and low[:-1].isdigit():
return low
if low.endswith("h") and low[:-1].isdigit():
return f"{low[:-1]}H"
if low.endswith("d") and low[:-1].isdigit():
return f"{low[:-1]}D"
return s
def _fetch_ohlcv_public(exchange_symbol, timeframe, limit=100, since_ms=None, end_ts_ms=None):
"""无 API Key 也可拉永续 K 线:OKX public_get_market_candles."""
inst = _okx_swap_inst_id(exchange_symbol)
bar = _okx_public_bar(timeframe)
lim = max(2, min(int(limit or 100), 300))
if not inst or not hasattr(exchange, "public_get_market_candles"):
return []
params = {"instId": inst, "bar": bar, "limit": str(lim)}
# after=ts → 早于 ts 的更旧数据,适合「截止到平仓」
if end_ts_ms:
try:
params["after"] = str(int(end_ts_ms))
except (TypeError, ValueError):
pass
try:
if not end_ts_ms:
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
else:
period = _timeframe_period_ms(timeframe)
since = int(end_ts_ms) - period * (lim + 10)
ohlcv = exchange.fetch_ohlcv(
exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 20
)
raw = (exchange.public_get_market_candles(params) or {}).get("data") or []
except Exception:
return []
out = []
for row in reversed(list(raw or [])):
if not row or len(row) < 5:
continue
try:
ts = int(row[0])
o = float(row[1])
h = float(row[2])
l = float(row[3])
c = float(row[4])
v = float(row[5]) if len(row) > 5 else 0.0
except (TypeError, ValueError, IndexError):
continue
if since_ms is not None:
try:
if ts < int(since_ms):
continue
except (TypeError, ValueError):
pass
if end_ts_ms is not None:
try:
if ts > int(end_ts_ms):
continue
except (TypeError, ValueError):
pass
out.append([ts, o, h, l, c, v])
if len(out) > lim:
return out[-lim:]
return out
def _fetch_ohlcv_safe(exchange_symbol, timeframe, limit=100, since_ms=None, end_ts_ms=None):
"""公开接口优先(模拟盘无密钥);失败再试 ccxt.fetch_ohlcv."""
pub = _fetch_ohlcv_public(
exchange_symbol,
timeframe,
limit=limit,
since_ms=since_ms,
end_ts_ms=end_ts_ms,
)
if pub:
return pub
try:
kwargs = {"timeframe": timeframe, "limit": int(limit or 100)}
if since_ms:
kwargs["since"] = max(0, int(since_ms))
return list(exchange.fetch_ohlcv(exchange_symbol, **kwargs) or [])
except Exception:
return []
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
lim = max(2, int(limit or ORDER_CHART_LIMIT))
ohlcv = _fetch_ohlcv_safe(
exchange_symbol,
timeframe,
limit=lim + 20,
end_ts_ms=end_ts_ms,
)
if not ohlcv and end_ts_ms:
ohlcv = _fetch_ohlcv_safe(exchange_symbol, timeframe, limit=lim)
rows = _ohlcv_to_rows(ohlcv)
if not rows:
return []
@@ -977,7 +1076,11 @@ def generate_multi_timeframe_chart_png(
ordered.append(tf)
timeframes = ordered[:4] if ordered else preferred_layout
ensure_markets_loaded()
try:
ensure_markets_loaded()
except Exception:
# 模拟盘无 API Key 时 load_markets 可能失败;公开 candles 不依赖 markets
pass
panels = []
cell_w, cell_h = 980, 520
end_ts_ms = None
@@ -1001,17 +1104,18 @@ def generate_multi_timeframe_chart_png(
now_ms=marker_payload.get("now_ts_ms"),
)
if win:
ohlcv = exchange.fetch_ohlcv(
ohlcv = _fetch_ohlcv_safe(
exchange_symbol,
timeframe=tf,
since=max(0, int(win["since_ms"])),
tf,
limit=int(win["fetch_limit"]),
since_ms=max(0, int(win["since_ms"])),
end_ts_ms=end_ts_ms,
)
rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win)
if not rows:
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
if not ohlcv and end_ts_ms:
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
ohlcv = _fetch_ohlcv_safe(exchange_symbol, tf, limit=limit)
rows = _ohlcv_to_rows(ohlcv)[-limit:]
except Exception:
rows = []
+13 -1
View File
@@ -12,7 +12,19 @@ from lib.sim.wallets_lib import InsufficientFunds, SimWallets
def _now() -> str:
return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
"""业务时区墙钟(默认 Asia/Shanghai);勿用 utcnow,否则复盘K线标记会偏 8 小时."""
try:
from lib.options.options_time_lib import options_app_now_str
return options_app_now_str()
except Exception:
from zoneinfo import ZoneInfo
tz = ZoneInfo(
(os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip()
or "Asia/Shanghai"
)
return datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
def _ticker_bid_ask(exchange: Any, symbol: str) -> tuple[float, float]:
+40
View File
@@ -0,0 +1,40 @@
"""OKX 公开 K 线小测(无网络):instId / bar 映射."""
from __future__ import annotations
import unittest
class OkxPublicOhlcvMappingTests(unittest.TestCase):
def test_swap_inst_and_bar(self):
# 避免 import 整站 app:直接复刻映射逻辑做契约校验
def swap_inst(exchange_symbol: str) -> str:
sym = str(exchange_symbol or "").strip().upper()
if not sym:
return ""
if "/" in sym:
base, rest = sym.split("/", 1)
quote = rest.split(":", 1)[0].strip() or "USDT"
return f"{base.strip()}-{quote}-SWAP"
if sym.endswith("-SWAP"):
return sym
return sym
def public_bar(timeframe: str) -> str:
low = str(timeframe or "").strip().lower()
if low.endswith("m") and low[:-1].isdigit():
return low
if low.endswith("h") and low[:-1].isdigit():
return f"{low[:-1]}H"
if low.endswith("d") and low[:-1].isdigit():
return f"{low[:-1]}D"
return low or "15m"
self.assertEqual(swap_inst("ETH/USDT:USDT"), "ETH-USDT-SWAP")
self.assertEqual(swap_inst("BTC/USDT:USDT"), "BTC-USDT-SWAP")
self.assertEqual(public_bar("15m"), "15m")
self.assertEqual(public_bar("1h"), "1H")
self.assertEqual(public_bar("4h"), "4H")
if __name__ == "__main__":
unittest.main()