中控行情区与 K 线本地库(15 天滚动、按需拉取)
新增行情区单图与周期切换,K 线优先读 hub_kline.db,不足时经各实例 /api/hub/ohlcv 补齐;无后台定时更新。含回滚标签说明与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+119
-1
@@ -13,6 +13,9 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from hub_kline_store import format_ohlcv_detail, resolve_chart_bars, retention_days
|
||||
from hub_ohlcv_lib import CHART_TIMEFRAMES, bar_limit_for_timeframe
|
||||
|
||||
from env_load import load_hub_dotenv
|
||||
|
||||
load_hub_dotenv()
|
||||
@@ -66,7 +69,7 @@ _allow_pub_raw = (os.getenv("HUB_ALLOW_PUBLIC") or "").strip().lower()
|
||||
# 云服务器 + 域名反代时设为 true:不做 IP 限制,仅靠 HUB_PASSWORD / 登录页保护
|
||||
HUB_ALLOW_PUBLIC = _allow_pub_raw in ("1", "true", "yes", "on")
|
||||
DIR = Path(__file__).resolve().parent
|
||||
HUB_BUILD = "20260526-hub-key3col"
|
||||
HUB_BUILD = "20260528-hub-market"
|
||||
HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
|
||||
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
||||
_board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower()
|
||||
@@ -258,6 +261,7 @@ def root_redirect():
|
||||
|
||||
|
||||
@app.get("/monitor")
|
||||
@app.get("/market")
|
||||
@app.get("/settings")
|
||||
def shell_pages():
|
||||
return _shell_page()
|
||||
@@ -294,6 +298,120 @@ def api_save_settings(body: SettingsBody):
|
||||
return {"ok": True, "settings": load_settings()}
|
||||
|
||||
|
||||
def _find_exchange_by_key(exchange_key: str) -> dict | None:
|
||||
key = (exchange_key or "").strip().lower()
|
||||
if not key:
|
||||
return None
|
||||
for ex in load_settings().get("exchanges") or []:
|
||||
if str(ex.get("key") or "").strip().lower() == key:
|
||||
return ex
|
||||
if str(ex.get("id") or "").strip() == exchange_key.strip():
|
||||
return ex
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_instance_ohlcv_sync(
|
||||
ex: dict,
|
||||
*,
|
||||
symbol: str,
|
||||
timeframe: str,
|
||||
since_ms: int | None,
|
||||
limit: int,
|
||||
) -> dict:
|
||||
base = (ex.get("flask_url") or "").rstrip("/")
|
||||
if not base:
|
||||
return {"ok": False, "msg": "未配置 flask_url"}
|
||||
params = {"symbol": symbol, "timeframe": timeframe, "limit": str(int(limit))}
|
||||
if since_ms is not None and int(since_ms) > 0:
|
||||
params["since_ms"] = str(int(since_ms))
|
||||
url = f"{base}/api/hub/ohlcv?{urlencode(params)}"
|
||||
try:
|
||||
with httpx.Client(timeout=HUB_FLASK_TIMEOUT) as client:
|
||||
r = client.get(url, headers=_hub_headers())
|
||||
if r.status_code >= 400:
|
||||
parsed = _parse_http_json_body(r)
|
||||
parsed.setdefault("ok", False)
|
||||
return parsed
|
||||
data = r.json() if r.content else {}
|
||||
return data if isinstance(data, dict) else {"ok": False, "msg": "无效 JSON"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
|
||||
|
||||
@app.get("/api/chart/meta")
|
||||
def api_chart_meta():
|
||||
tfs = ["1m", "5m", "15m", "1h", "4h", "1d", "1w"]
|
||||
exchanges = []
|
||||
for ex in enabled_exchanges(load_settings()):
|
||||
exchanges.append(
|
||||
{
|
||||
"id": ex.get("id"),
|
||||
"key": ex.get("key"),
|
||||
"name": ex.get("name"),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"timeframes": [tf for tf in tfs if tf in CHART_TIMEFRAMES],
|
||||
"retention_days": retention_days(),
|
||||
"limits": {tf: bar_limit_for_timeframe(tf) for tf in tfs if tf in CHART_TIMEFRAMES},
|
||||
"exchanges": exchanges,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/chart/ohlcv")
|
||||
def api_chart_ohlcv(
|
||||
exchange_key: str = "",
|
||||
symbol: str = "",
|
||||
timeframe: str = "5m",
|
||||
refresh: str = "",
|
||||
):
|
||||
ex = _find_exchange_by_key(exchange_key)
|
||||
if not ex:
|
||||
raise HTTPException(status_code=400, detail="交易所不存在")
|
||||
if not ex.get("enabled"):
|
||||
raise HTTPException(status_code=400, detail="该交易所未启用")
|
||||
sym = (symbol or "").strip().upper()
|
||||
if not sym:
|
||||
raise HTTPException(status_code=400, detail="请输入币种")
|
||||
ex_key = str(ex.get("key") or "").strip().lower()
|
||||
force = (refresh or "").strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
def remote_fetch(**kwargs):
|
||||
return _fetch_instance_ohlcv_sync(
|
||||
ex,
|
||||
symbol=kwargs.get("symbol") or sym,
|
||||
timeframe=kwargs.get("timeframe") or timeframe,
|
||||
since_ms=kwargs.get("since_ms"),
|
||||
limit=int(kwargs.get("limit") or bar_limit_for_timeframe(timeframe)),
|
||||
)
|
||||
|
||||
result = resolve_chart_bars(
|
||||
ex_key,
|
||||
sym,
|
||||
timeframe,
|
||||
remote_fetch,
|
||||
force_refresh=force,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "K线加载失败")
|
||||
tick = result.get("price_tick")
|
||||
last = result["candles"][-1] if result.get("candles") else None
|
||||
result["ohlcv"] = format_ohlcv_detail(
|
||||
{
|
||||
"open": last.get("open") if last else None,
|
||||
"high": last.get("high") if last else None,
|
||||
"low": last.get("low") if last else None,
|
||||
"close": last.get("close") if last else None,
|
||||
"volume": last.get("volume") if last else None,
|
||||
}
|
||||
if last
|
||||
else None,
|
||||
tick,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/api/settings/meta")
|
||||
def api_settings_meta():
|
||||
po = public_origin()
|
||||
|
||||
Reference in New Issue
Block a user