模拟盘无 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]
|
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):
|
def _okx_swap_inst_id(exchange_symbol):
|
||||||
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
"""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:
|
try:
|
||||||
if not end_ts_ms:
|
raw = (exchange.public_get_market_candles(params) or {}).get("data") or []
|
||||||
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
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
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)
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
if not rows:
|
if not rows:
|
||||||
return []
|
return []
|
||||||
@@ -977,7 +1076,11 @@ def generate_multi_timeframe_chart_png(
|
|||||||
ordered.append(tf)
|
ordered.append(tf)
|
||||||
timeframes = ordered[:4] if ordered else preferred_layout
|
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 = []
|
panels = []
|
||||||
cell_w, cell_h = 980, 520
|
cell_w, cell_h = 980, 520
|
||||||
end_ts_ms = None
|
end_ts_ms = None
|
||||||
@@ -1001,17 +1104,18 @@ def generate_multi_timeframe_chart_png(
|
|||||||
now_ms=marker_payload.get("now_ts_ms"),
|
now_ms=marker_payload.get("now_ts_ms"),
|
||||||
)
|
)
|
||||||
if win:
|
if win:
|
||||||
ohlcv = exchange.fetch_ohlcv(
|
ohlcv = _fetch_ohlcv_safe(
|
||||||
exchange_symbol,
|
exchange_symbol,
|
||||||
timeframe=tf,
|
tf,
|
||||||
since=max(0, int(win["since_ms"])),
|
|
||||||
limit=int(win["fetch_limit"]),
|
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)
|
rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win)
|
||||||
if not rows:
|
if not rows:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
||||||
if not ohlcv and 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:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
except Exception:
|
except Exception:
|
||||||
rows = []
|
rows = []
|
||||||
|
|||||||
+13
-1
@@ -12,7 +12,19 @@ from lib.sim.wallets_lib import InsufficientFunds, SimWallets
|
|||||||
|
|
||||||
|
|
||||||
def _now() -> str:
|
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]:
|
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