模拟盘无 API Key 时复盘K线改为优先走 OKX 公开行情接口。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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:
|
||||
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
|
||||
)
|
||||
params["after"] = str(int(end_ts_ms))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
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
|
||||
|
||||
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
@@ -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]:
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user