feat(instance): add exchange account ledger tab with SSE sync
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""三所统一:账户流水路由 + 后台同步安装."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
from flask import Flask, Response, jsonify, request, session, stream_with_context
|
||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
|
||||
from lib.account_ledger.account_ledger_db import ensure_account_ledger_tables, query_entries
|
||||
from lib.account_ledger.account_ledger_normalize import (
|
||||
ACCOUNT_FUNDING,
|
||||
ACCOUNT_TRADING,
|
||||
PAGE_SIZE,
|
||||
VALID_ACCOUNTS,
|
||||
)
|
||||
from lib.account_ledger.account_ledger_sync import account_ledger_store
|
||||
from lib.common.history_window_lib import resolve_list_window
|
||||
|
||||
|
||||
def attach_account_ledger_templates(app: Flask, repo_root: str) -> None:
|
||||
tpl_dir = os.path.join(repo_root, "lib", "account_ledger", "templates")
|
||||
if not os.path.isdir(tpl_dir):
|
||||
return
|
||||
existing = app.jinja_loader
|
||||
loaders = [FileSystemLoader(tpl_dir)]
|
||||
if existing is not None:
|
||||
if isinstance(existing, ChoiceLoader):
|
||||
loaders = list(existing.loaders) + loaders
|
||||
else:
|
||||
loaders.insert(0, existing)
|
||||
app.jinja_loader = ChoiceLoader(loaders)
|
||||
|
||||
|
||||
def _build_fetch_fn(exchange_key: str, app_module: Any) -> Callable:
|
||||
ex_key = (exchange_key or "").strip().lower()
|
||||
exchange = getattr(app_module, "exchange", None)
|
||||
ensure_markets = getattr(app_module, "ensure_markets_loaded", None)
|
||||
|
||||
def _fetch(*, start_ms: int, end_ms: int):
|
||||
if exchange is None:
|
||||
return [], ["exchange missing"]
|
||||
if ex_key == "okx":
|
||||
from lib.exchange.okx_ledger_lib import fetch_okx_account_ledger
|
||||
|
||||
return fetch_okx_account_ledger(
|
||||
exchange,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
ensure_markets=ensure_markets,
|
||||
)
|
||||
if ex_key == "binance":
|
||||
from lib.exchange.binance_ledger_lib import fetch_binance_account_ledger
|
||||
|
||||
return fetch_binance_account_ledger(
|
||||
exchange,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
ensure_markets=ensure_markets,
|
||||
)
|
||||
from lib.exchange.gate_ledger_lib import fetch_gate_account_ledger
|
||||
|
||||
return fetch_gate_account_ledger(
|
||||
exchange,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
ensure_markets=ensure_markets,
|
||||
)
|
||||
|
||||
return _fetch
|
||||
|
||||
|
||||
def _currencies_for_exchange(exchange_key: str) -> list[str]:
|
||||
if (exchange_key or "").strip().lower() == "okx":
|
||||
return ["USDT", "USDC"]
|
||||
return ["USDT"]
|
||||
|
||||
|
||||
def install_account_ledger(
|
||||
app: Flask,
|
||||
repo_root: str,
|
||||
app_module: Any,
|
||||
*,
|
||||
exchange_key: str = "",
|
||||
) -> None:
|
||||
ex = (exchange_key or "").strip().lower()
|
||||
if not ex:
|
||||
mod_name = getattr(app_module, "__name__", "") or ""
|
||||
if "okx" in mod_name.lower():
|
||||
ex = "okx"
|
||||
elif "binance" in mod_name.lower():
|
||||
ex = "binance"
|
||||
else:
|
||||
ex = "gate"
|
||||
exchange_key = ex
|
||||
|
||||
attach_account_ledger_templates(app, repo_root)
|
||||
get_db = app_module.get_db
|
||||
login_required = app_module.login_required
|
||||
|
||||
# 初始化表
|
||||
try:
|
||||
conn = get_db()
|
||||
try:
|
||||
ensure_account_ledger_tables(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
account_ledger_store.configure(
|
||||
get_db=get_db,
|
||||
fetch_fn=_build_fetch_fn(exchange_key, app_module),
|
||||
exchange_key=str(exchange_key),
|
||||
)
|
||||
account_ledger_store.start()
|
||||
app.extensions["account_ledger_exchange"] = str(exchange_key).lower()
|
||||
|
||||
def _list_window():
|
||||
resolve = getattr(app_module, "_list_window_from_request", None)
|
||||
if callable(resolve):
|
||||
return resolve()
|
||||
return resolve_list_window(request.args, session)
|
||||
|
||||
@app.route("/api/account_ledger")
|
||||
@login_required
|
||||
def api_account_ledger():
|
||||
account = (request.args.get("account") or ACCOUNT_FUNDING).strip().lower()
|
||||
if account not in VALID_ACCOUNTS:
|
||||
account = ACCOUNT_FUNDING
|
||||
try:
|
||||
page = int(request.args.get("page") or 1)
|
||||
except Exception:
|
||||
page = 1
|
||||
win = _list_window()
|
||||
start_ms = int(win.get("start_ms") or 0)
|
||||
end_ms = int(win.get("end_ms") or 0)
|
||||
ccys = _currencies_for_exchange(app.extensions.get("account_ledger_exchange") or "")
|
||||
conn = get_db()
|
||||
try:
|
||||
ensure_account_ledger_tables(conn)
|
||||
data = query_entries(
|
||||
conn,
|
||||
account=account,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
page=page,
|
||||
page_size=PAGE_SIZE,
|
||||
currencies=ccys,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
st = account_ledger_store.status_dict()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"account": account,
|
||||
"window": {
|
||||
"preset": win.get("preset"),
|
||||
"label": win.get("label"),
|
||||
"start_ms": start_ms,
|
||||
"end_ms": end_ms,
|
||||
},
|
||||
"currencies": ccys,
|
||||
**data,
|
||||
**st,
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/account_ledger/stream")
|
||||
@login_required
|
||||
def api_account_ledger_stream():
|
||||
return Response(
|
||||
stream_with_context(account_ledger_store.iter_sse()),
|
||||
mimetype="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
@app.route("/api/account_ledger/refresh", methods=["POST"])
|
||||
@login_required
|
||||
def api_account_ledger_refresh():
|
||||
win = _list_window()
|
||||
body = request.get_json(silent=True) or {}
|
||||
start_ms = body.get("start_ms", win.get("start_ms"))
|
||||
end_ms = body.get("end_ms", win.get("end_ms"))
|
||||
try:
|
||||
start_i = int(start_ms) if start_ms is not None else None
|
||||
end_i = int(end_ms) if end_ms is not None else None
|
||||
except Exception:
|
||||
start_i, end_i = None, None
|
||||
result = account_ledger_store.sync_once(
|
||||
reason="manual", start_ms=start_i, end_ms=end_i
|
||||
)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/account_ledger")
|
||||
@login_required
|
||||
def account_ledger_page():
|
||||
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
|
||||
|
||||
redir = redirect_to_embed_shell_if_enabled("account_ledger")
|
||||
if redir is not None:
|
||||
return redir
|
||||
return app_module.render_main_page("account_ledger")
|
||||
Reference in New Issue
Block a user