diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 33fd14e..6e86542 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from .auth_routes import router as auth_router from .backup_routes import router as backup_router +from .funds import router as funds_router from .market import router as market_router from .plan import router as plan_router from .settings import router as settings_router @@ -16,5 +17,6 @@ router.include_router(sim_router) router.include_router(plan_router) router.include_router(trades_router) router.include_router(stats_router) +router.include_router(funds_router) router.include_router(settings_router) router.include_router(backup_router) diff --git a/backend/app/api/funds.py b/backend/app/api/funds.py new file mode 100644 index 0000000..3b18d92 --- /dev/null +++ b/backend/app/api/funds.py @@ -0,0 +1,229 @@ +"""资金摘要 / 兑换 / 划转(SIM 本地钱包 + LIVE OKX)。""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Annotated, Any, Literal +from zoneinfo import ZoneInfo + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field + +from ..config import get_settings +from ..live.okx_funds import OkxFundsClient, usdc_usdt_mid_rate +from ..models.db import get_db +from ..sim.funds_wallets import SimFundsWallets +from ..sim.ledger import Ledger +from ..strategy.engine import get_engine +from .auth import require_user + +router = APIRouter(prefix="/api/funds", tags=["funds"]) + +SH = ZoneInfo("Asia/Shanghai") + + +def _pl_ratio(pnls: list[float]) -> float | None: + wins = [x for x in pnls if x > 0] + losses = [abs(x) for x in pnls if x < 0] + if not wins or not losses: + return None + avg_w = sum(wins) / len(wins) + avg_l = sum(losses) / len(losses) + if avg_l <= 1e-12: + return None + return round(avg_w / avg_l, 2) + + +def _fmt_opt(usdc: float | None, usdt: float | None) -> str: + parts: list[str] = [] + if usdc is not None: + parts.append(f"{usdc:.2f} USDC") + if usdt is not None and abs(usdt) > 1e-8: + parts.append(f"{usdt:.2f} USDT") + return " + ".join(parts) if parts else "—" + + +class ConvertBody(BaseModel): + direction: Literal["usdt_to_usdc", "usdc_to_usdt"] + amount: float = Field(gt=0) + + +class TransferBody(BaseModel): + ccy: Literal["USDT", "USDC", "usdt", "usdc"] = "USDC" + amount: float = Field(gt=0) + from_account: str + to_account: str + + +@router.get("/summary") +async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[str, Any]: + s = get_settings() + db = get_db() + eng = get_engine() + st = eng.state() + # 不在摘要里强调 SIM/LIVE 文案;仅给前端内部用 + mode = "LIVE" if not s.is_sim else "SIM" + exchange = str(st.get("exchange") or s.exchange or "okx").upper() + trading_day = datetime.now(SH).strftime("%Y-%m-%d") + + closed = db.fetchall( + "SELECT realized_pnl, close_at_ms FROM groups WHERE status='closed'" + ) + pnls = [float(r["realized_pnl"] or 0) for r in closed] + n = len(pnls) + wins = sum(1 for x in pnls if x > 0) + win_rate = (wins / n) if n else 0.0 + + # 当日成交组 + day_prefix = trading_day.replace("-", "") + day_groups = db.fetchall( + "SELECT realized_pnl FROM groups WHERE group_id LIKE ? AND status='closed'", + (f"G-{day_prefix}-%",), + ) + day_n = len(day_groups) + + pos = st.get("position") or {} + upl = pos + realtime = None + if str(pos.get("status") or "") == "open": + realtime = float(pos.get("net_pnl") or 0) + + if s.is_sim: + wallets = SimFundsWallets(db) + w = wallets.snapshot() + # 若钱包全 0 但账本有权益,补种一次 + if wallets.total_usdt_equiv(w) < 1e-9: + eq = float(Ledger(db).snapshot().get("equity") or 0) + if eq > 0: + w = wallets.reset_from_equity(eq) + funding_usdt = float(w["funding_usdt"]) + trading_usdt = float(w["trading_usdt"]) + opt_f_usdc = float(w["options_funding_usdc"]) + opt_t_usdc = float(w["options_trading_usdc"]) + opt_f_usdt = float(w["options_funding_usdt"]) + opt_t_usdt = float(w["options_trading_usdt"]) + total = wallets.total_usdt_equiv(w) + rate = usdc_usdt_mid_rate() + else: + rate = usdc_usdt_mid_rate() + funding_usdt = trading_usdt = None + opt_f_usdc = opt_t_usdc = opt_f_usdt = opt_t_usdt = None + total = None + if exchange == "OKX": + client = OkxFundsClient() + try: + bal = client.fetch_balances() + funding_usdt = bal.get("funding_usdt") + trading_usdt = bal.get("trading_usdt") + opt_f_usdc = bal.get("options_funding_usdc") + opt_t_usdc = bal.get("options_trading_usdc") + opt_f_usdt = bal.get("options_funding_usdt") + opt_t_usdt = bal.get("options_trading_usdt") + parts = [ + funding_usdt, + trading_usdt, + opt_f_usdc, + opt_t_usdc, + opt_f_usdt, + opt_t_usdt, + ] + vals = [float(x) for x in parts if x is not None] + total = round(sum(vals), 2) if vals else None + except Exception as e: + return { + "ok": False, + "mode": mode, + "exchange": exchange, + "detail": str(e), + } + finally: + client.close() + else: + # 非 OKX LIVE:回退本地账本 + led = Ledger(db).snapshot() + trading_usdt = float(led["equity"]) + total = trading_usdt + + return { + "ok": True, + "mode": mode, + "exchange": exchange, + "trading_day": trading_day, + "total_trades": day_n if day_n else n, + "win_rate": win_rate, + "profit_loss_ratio": _pl_ratio(pnls), + "total_funds": total, + "funding_usdt": funding_usdt, + "trading_usdt": trading_usdt, + "options_funding_usdc": opt_f_usdc, + "options_trading_usdc": opt_t_usdc, + "options_funding_usdt": opt_f_usdt, + "options_trading_usdt": opt_t_usdt, + "options_funding_label": _fmt_opt(opt_f_usdc, opt_f_usdt), + "options_trading_label": _fmt_opt(opt_t_usdc, opt_t_usdt), + "realtime_pnl": realtime, + "usdc_usdt_rate": rate, + "updated_at_ms": int(datetime.now(timezone.utc).timestamp() * 1000), + } + + +@router.post("/convert") +async def funds_convert( + body: ConvertBody, _user: Annotated[str, Depends(require_user)] +) -> dict[str, Any]: + s = get_settings() + rate = usdc_usdt_mid_rate() + if s.is_sim: + r = SimFundsWallets(get_db()).convert( + direction=body.direction, amount=float(body.amount), rate=rate + ) + if not r.get("ok"): + raise HTTPException(status_code=400, detail=r.get("detail") or "兑换失败") + return r + + if str(s.exchange).lower() != "okx": + raise HTTPException(status_code=400, detail="当前仅 OKX 支持 USDC/USDT 兑换") + client = OkxFundsClient() + try: + r = client.spot_swap_usdt_usdc( + direction=body.direction, amount=float(body.amount) + ) + finally: + client.close() + if not r.get("ok"): + raise HTTPException(status_code=400, detail=r.get("detail") or "兑换失败") + return r + + +@router.post("/transfer") +async def funds_transfer( + body: TransferBody, _user: Annotated[str, Depends(require_user)] +) -> dict[str, Any]: + s = get_settings() + ccy = str(body.ccy).upper() + if s.is_sim: + r = SimFundsWallets(get_db()).transfer( + ccy=ccy, + amount=float(body.amount), + from_account=body.from_account, + to_account=body.to_account, + ) + if not r.get("ok"): + raise HTTPException(status_code=400, detail=r.get("detail") or "划转失败") + return r + + if str(s.exchange).lower() != "okx": + raise HTTPException(status_code=400, detail="当前仅 OKX 支持账户划转") + client = OkxFundsClient() + try: + r = client.transfer( + ccy=ccy, + amount=float(body.amount), + from_account=body.from_account, + to_account=body.to_account, + ) + finally: + client.close() + if not r.get("ok"): + raise HTTPException(status_code=400, detail=r.get("detail") or "划转失败") + return r diff --git a/backend/app/live/okx_funds.py b/backend/app/live/okx_funds.py new file mode 100644 index 0000000..6b8996f --- /dev/null +++ b/backend/app/live/okx_funds.py @@ -0,0 +1,191 @@ +"""OKX 资金:余额 / USDT↔USDC 现货兑换 / 账户划转(对齐 crypto_monitor)。""" + +from __future__ import annotations + +import logging +from typing import Any + +from .okx_trade import OkxTradeClient + +logger = logging.getLogger(__name__) + +# OKX acct: 6=资金, 18=交易 +_ACCT_CODE = { + "funding": "6", + "trading": "18", + "spot": "18", + "options_funding": "6", + "options_trading": "18", +} + + +def _f(v: Any) -> float | None: + try: + if v is None or v == "": + return None + return float(v) + except (TypeError, ValueError): + return None + + +class OkxFundsClient: + def __init__(self, trade: OkxTradeClient | None = None) -> None: + self.trade = trade or OkxTradeClient() + + def close(self) -> None: + self.trade.close() + + def fetch_balances(self) -> dict[str, float | None]: + """ + 拉取资金账户 + 交易账户 USDT/USDC。 + 资金:GET /api/v5/asset/balances + 交易:GET /api/v5/account/balance + """ + out: dict[str, float | None] = { + "funding_usdt": None, + "funding_usdc": None, + "trading_usdt": None, + "trading_usdc": None, + "options_funding_usdc": None, + "options_trading_usdc": None, + "options_funding_usdt": None, + "options_trading_usdt": None, + } + try: + rows = self.trade._request("GET", "/api/v5/asset/balances") + for row in rows: + ccy = str(row.get("ccy") or "").upper() + bal = _f(row.get("bal")) or _f(row.get("availBal")) + if ccy == "USDT": + out["funding_usdt"] = bal + out["options_funding_usdt"] = bal + elif ccy == "USDC": + out["funding_usdc"] = bal + out["options_funding_usdc"] = bal + except Exception as e: + logger.warning("OKX asset balances failed: %s", e) + + try: + rows = self.trade._request("GET", "/api/v5/account/balance") + for block in rows: + details = block.get("details") or [] + if not isinstance(details, list): + continue + for row in details: + if not isinstance(row, dict): + continue + ccy = str(row.get("ccy") or "").upper() + eq = _f(row.get("eq")) or _f(row.get("cashBal")) or _f(row.get("availBal")) + if ccy == "USDT": + out["trading_usdt"] = eq + out["options_trading_usdt"] = eq + elif ccy == "USDC": + out["trading_usdc"] = eq + out["options_trading_usdc"] = eq + except Exception as e: + logger.warning("OKX account balance failed: %s", e) + + return out + + def spot_swap_usdt_usdc(self, *, direction: str, amount: float) -> dict[str, Any]: + """现货市价兑换 USDC-USDT(与 crypto_monitor spot_market_swap_usdt_usdc 同口径)。""" + amt = float(amount) + if amt <= 0: + return {"ok": False, "detail": "数量须大于 0"} + d = (direction or "").strip().lower() + inst_id = "USDC-USDT" + if d == "usdt_to_usdc": + body = { + "instId": inst_id, + "tdMode": "cash", + "side": "buy", + "ordType": "market", + "sz": str(amt), + "tgtCcy": "quote_ccy", + } + elif d == "usdc_to_usdt": + body = { + "instId": inst_id, + "tdMode": "cash", + "side": "sell", + "ordType": "market", + "sz": str(amt), + "tgtCcy": "base_ccy", + } + else: + return {"ok": False, "detail": "direction 须为 usdt_to_usdc 或 usdc_to_usdt"} + try: + rows = self.trade._request("POST", "/api/v5/trade/order", body) + if not rows: + return {"ok": False, "detail": "兑换下单无返回"} + row = rows[0] + if str(row.get("sCode") or "0") not in ("0", ""): + return { + "ok": False, + "detail": str(row.get("sMsg") or row.get("sCode") or "兑换失败"), + "raw": row, + } + return {"ok": True, "detail": "converted", "data": row} + except Exception as e: + return {"ok": False, "detail": str(e)} + + def transfer( + self, + *, + ccy: str, + amount: float, + from_account: str, + to_account: str, + ) -> dict[str, Any]: + """同一 API Key 下资金↔交易划转。""" + amt = float(amount) + if amt <= 0: + return {"ok": False, "detail": "划转金额须大于 0"} + fa = (from_account or "").strip().lower() + ta = (to_account or "").strip().lower() + if fa == ta: + return {"ok": False, "detail": "来源与目标账户不能相同"} + from_code = _ACCT_CODE.get(fa) + to_code = _ACCT_CODE.get(ta) + if not from_code or not to_code: + return {"ok": False, "detail": "账户须为 funding/trading(或 options_* 别名)"} + body = { + "ccy": str(ccy).upper(), + "amt": str(amt), + "from": from_code, + "to": to_code, + "type": "0", + } + try: + rows = self.trade._request("POST", "/api/v5/asset/transfer", body) + if not rows: + return {"ok": False, "detail": "划转无返回"} + return {"ok": True, "detail": "transferred", "data": rows[0]} + except Exception as e: + return {"ok": False, "detail": str(e)} + + +def usdc_usdt_mid_rate() -> float: + """公共盘口中间价:1 USDC ≈ ? USDT;失败则 1.0。""" + try: + import httpx + from ..config import get_settings + + s = get_settings() + proxy = (s.okx_http_proxy or "").strip() or None + with httpx.Client(base_url=s.okx_rest_base.rstrip("/"), timeout=8.0, proxy=proxy) as c: + r = c.get("/api/v5/market/ticker", params={"instId": "USDC-USDT"}) + r.raise_for_status() + rows = (r.json() or {}).get("data") or [] + if not rows: + return 1.0 + bid = _f(rows[0].get("bidPx")) + ask = _f(rows[0].get("askPx")) + last = _f(rows[0].get("last")) + if bid and ask and bid > 0 and ask > 0: + return (bid + ask) / 2.0 + if last and last > 0: + return last + except Exception as e: + logger.warning("USDC-USDT mid failed: %s", e) + return 1.0 diff --git a/backend/app/models/db.py b/backend/app/models/db.py index 04cf465..0e12d78 100644 --- a/backend/app/models/db.py +++ b/backend/app/models/db.py @@ -123,6 +123,16 @@ CREATE TABLE IF NOT EXISTS residual_options ( note TEXT, FOREIGN KEY(group_id) REFERENCES groups(group_id) ); +CREATE TABLE IF NOT EXISTS funds_wallets ( + id INTEGER PRIMARY KEY CHECK (id = 1), + funding_usdt REAL NOT NULL DEFAULT 0, + trading_usdt REAL NOT NULL DEFAULT 0, + options_funding_usdc REAL NOT NULL DEFAULT 0, + options_trading_usdc REAL NOT NULL DEFAULT 0, + options_funding_usdt REAL NOT NULL DEFAULT 0, + options_trading_usdt REAL NOT NULL DEFAULT 0, + updated_at_ms INTEGER NOT NULL +); """ @@ -192,6 +202,19 @@ class Database: "INSERT INTO strategy_state(id, running, phase, rounds_done, updated_at_ms) VALUES (1,0,'idle',0,?)", (now,), ) + fw = self._conn.execute("SELECT id FROM funds_wallets WHERE id=1").fetchone() + if fw is None: + led = self._conn.execute( + "SELECT equity FROM ledger_meta WHERE id=1" + ).fetchone() + eq = float(led["equity"]) if led else float(s.initial_equity) + self._conn.execute( + """INSERT INTO funds_wallets( + id, funding_usdt, trading_usdt, options_funding_usdc, options_trading_usdc, + options_funding_usdt, options_trading_usdt, updated_at_ms + ) VALUES (1,?,0,0,0,0,0,?)""", + (eq, now), + ) defaults = { "fee_rate": str(s.fee_rate), "initial_equity": str(s.initial_equity), diff --git a/backend/app/sim/funds_wallets.py b/backend/app/sim/funds_wallets.py new file mode 100644 index 0000000..afa804a --- /dev/null +++ b/backend/app/sim/funds_wallets.py @@ -0,0 +1,202 @@ +"""SIM 多账户资金:资金/交易 USDT + 期权资金/交易 USDC(对齐 OKX 展示)。""" + +from __future__ import annotations + +import time +from typing import Any + +from ..models.db import Database, get_db + +WALLET_KEYS = ( + "funding_usdt", + "trading_usdt", + "options_funding_usdc", + "options_trading_usdc", + "options_funding_usdt", + "options_trading_usdt", +) + +# 划转账户映射 +_ACCT_MAP = { + ("funding", "usdt"): "funding_usdt", + ("trading", "usdt"): "trading_usdt", + ("options_funding", "usdc"): "options_funding_usdc", + ("options_trading", "usdc"): "options_trading_usdc", + ("options_funding", "usdt"): "options_funding_usdt", + ("options_trading", "usdt"): "options_trading_usdt", + # 简化别名:funding/trading + usdc → 期权侧 + ("funding", "usdc"): "options_funding_usdc", + ("trading", "usdc"): "options_trading_usdc", +} + + +def _now_ms() -> int: + return int(time.time() * 1000) + + +class SimFundsWallets: + def __init__(self, db: Database | None = None) -> None: + self.db = db or get_db() + + def snapshot(self) -> dict[str, float]: + row = self.db.fetchone("SELECT * FROM funds_wallets WHERE id=1") + if row is None: + return {k: 0.0 for k in WALLET_KEYS} + return {k: float(row[k] or 0) for k in WALLET_KEYS} + + def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float: + """USDC 按 1:1 计入总资金(与 crypto_monitor 一致)。""" + s = snap or self.snapshot() + return round(sum(float(s.get(k) or 0) for k in WALLET_KEYS), 8) + + def reset_from_equity(self, equity: float) -> dict[str, float]: + """重置:全部放入资金账户 USDT。""" + amt = max(0.0, float(equity)) + now = _now_ms() + self.db.execute( + """UPDATE funds_wallets SET + funding_usdt=?, trading_usdt=0, options_funding_usdc=0, options_trading_usdc=0, + options_funding_usdt=0, options_trading_usdt=0, updated_at_ms=? + WHERE id=1""", + (amt, now), + ) + return self.snapshot() + + def _set(self, **kwargs: float) -> dict[str, float]: + snap = self.snapshot() + for k, v in kwargs.items(): + if k in WALLET_KEYS: + snap[k] = float(v) + now = _now_ms() + self.db.execute( + """UPDATE funds_wallets SET + funding_usdt=?, trading_usdt=?, options_funding_usdc=?, options_trading_usdc=?, + options_funding_usdt=?, options_trading_usdt=?, updated_at_ms=? + WHERE id=1""", + ( + snap["funding_usdt"], + snap["trading_usdt"], + snap["options_funding_usdc"], + snap["options_trading_usdc"], + snap["options_funding_usdt"], + snap["options_trading_usdt"], + now, + ), + ) + return snap + + def mirror_cash(self, amount: float, *, kind: str) -> None: + """策略账本变动时镜像到对应钱包(SIM)。""" + amt = float(amount) + if abs(amt) < 1e-12: + return + snap = self.snapshot() + k = (kind or "").lower() + if "option" in k: + key = "options_trading_usdc" + elif "perp" in k or "funding" in k: + key = "trading_usdt" + else: + key = "trading_usdt" + snap[key] = float(snap.get(key) or 0) + amt + # 允许短暂为负(与 ledger allow_negative 对齐时由调用方保证);展示侧夹到合理范围不在此做 + self._set(**snap) + + def sync_ledger_equity(self) -> float: + """兑换/划转后把总权益同步到 ledger_meta(不重置钱包分配)。""" + total = self.total_usdt_equiv() + now = _now_ms() + self.db.execute( + "UPDATE ledger_meta SET equity=?, available=?, updated_at_ms=? WHERE id=1", + (total, total, now), + ) + return total + + def convert( + self, + *, + direction: str, + amount: float, + rate: float = 1.0, + ) -> dict[str, Any]: + """ + SIM 兑换(默认在「资金账户」内 USDT↔USDC,对齐 crypto_monitor 主路径)。 + direction: usdt_to_usdc | usdc_to_usdt + rate: 1 USDC = rate USDT(默认 1.0) + """ + amt = float(amount) + if amt <= 0: + return {"ok": False, "detail": "数量须大于 0"} + r = float(rate) if rate and rate > 0 else 1.0 + d = (direction or "").strip().lower() + snap = self.snapshot() + if d == "usdt_to_usdc": + src = float(snap["funding_usdt"]) + if amt > src + 1e-9: + return {"ok": False, "detail": f"资金账户 USDT 不足(可用 {src:.4f})"} + usdc = amt / r + snap["funding_usdt"] = src - amt + snap["options_funding_usdc"] = float(snap["options_funding_usdc"]) + usdc + elif d == "usdc_to_usdt": + src = float(snap["options_funding_usdc"]) + if amt > src + 1e-9: + return {"ok": False, "detail": f"期权资金账户 USDC 不足(可用 {src:.4f})"} + usdt = amt * r + snap["options_funding_usdc"] = src - amt + snap["funding_usdt"] = float(snap["funding_usdt"]) + usdt + else: + return {"ok": False, "detail": "direction 须为 usdt_to_usdc 或 usdc_to_usdt"} + self._set(**snap) + total = self.sync_ledger_equity() + return { + "ok": True, + "detail": "converted", + "direction": d, + "amount": amt, + "rate": r, + "wallets": self.snapshot(), + "total_usdt_equiv": total, + } + + def transfer( + self, + *, + ccy: str, + amount: float, + from_account: str, + to_account: str, + ) -> dict[str, Any]: + """SIM 划转:funding/trading/options_funding/options_trading × USDT|USDC。""" + amt = float(amount) + if amt <= 0: + return {"ok": False, "detail": "划转金额须大于 0"} + ccy_l = (ccy or "USDC").strip().lower() + fa = (from_account or "").strip().lower() + ta = (to_account or "").strip().lower() + if fa == ta: + return {"ok": False, "detail": "来源与目标账户不能相同"} + src_key = _ACCT_MAP.get((fa, ccy_l)) + dst_key = _ACCT_MAP.get((ta, ccy_l)) + if not src_key or not dst_key: + return { + "ok": False, + "detail": "账户须为 funding/trading/options_funding/options_trading,币种 USDT|USDC", + } + snap = self.snapshot() + src_bal = float(snap[src_key]) + if amt > src_bal + 1e-9: + return {"ok": False, "detail": f"{src_key} 余额不足(可用 {src_bal:.4f})"} + snap[src_key] = src_bal - amt + snap[dst_key] = float(snap[dst_key]) + amt + self._set(**snap) + total = self.sync_ledger_equity() + return { + "ok": True, + "detail": "transferred", + "ccy": ccy_l.upper(), + "amount": amt, + "from": fa, + "to": ta, + "wallets": self.snapshot(), + "total_usdt_equiv": total, + } diff --git a/backend/app/sim/ledger.py b/backend/app/sim/ledger.py index e3c5e29..0951e85 100644 --- a/backend/app/sim/ledger.py +++ b/backend/app/sim/ledger.py @@ -50,7 +50,15 @@ class Ledger: (group_id, kind, float(amount), equity, note, now), ) self.db._conn.commit() - return equity + try: + from ..config import get_settings + from .funds_wallets import SimFundsWallets + + if get_settings().is_sim: + SimFundsWallets(self.db).mirror_cash(float(amount), kind=kind) + except Exception: + pass + return equity def reset_equity(self, amount: float, *, note: str = "重置模拟资金") -> float: """将权益与可用资金重置为 amount(reserved 清零)。须在无持仓时调用。""" @@ -68,7 +76,15 @@ class Ledger: (None, "reset", amt, amt, note, now), ) self.db._conn.commit() - return amt + try: + from ..config import get_settings + from .funds_wallets import SimFundsWallets + + if get_settings().is_sim: + SimFundsWallets(self.db).reset_from_equity(amt) + except Exception: + pass + return amt def get_setting_float(self, key: str, default: float) -> float: v = self.db.get_setting(key) diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index e2f20af..bfb67e0 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -126,6 +126,7 @@ class StrategyEngine: "ledger": self.ledger.snapshot(), "mode": "SIM" if s.is_sim else "LIVE", "sim": s.is_sim, + "exchange": str(s.exchange or "okx").lower(), "live_ready": (live_ready()[0] if not s.is_sim else True), "live_ready_reason": (live_ready()[1] if not s.is_sim else "sim"), "show_manual_trade_buttons": self.ledger.get_setting_bool( diff --git a/docs/更新说明.md b/docs/更新说明.md index 64d9dac..ef08907 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,17 @@ --- +## 2026-07-29 — 资金条 + USDT/USDC 兑换划转(SIM/LIVE) + +### 变更 + +1. 顶栏增加资金统计条(交易所/交易日/胜率/总资金/资金·交易·期权账户/实时盈亏),**不标注模拟盘**。 +2. SIM:多账户钱包(USDT 资金/交易 + USDC 期权资金/交易);设置页可兑换、划转。 +3. LIVE(OKX):拉交易所余额;现货市价兑换 USDC-USDT;资金↔交易划转(对齐 crypto_monitor)。 +4. USDC 计入总资金按 1:1。 + +--- + ## 2026-07-29 — 统计拆分手续费;LIVE 不展示/不计滑点 ### 变更 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e371577..db8b329 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { getUsername, startAuthTokenAutoRefresh, } from "./api/client"; +import FundsBar from "./components/FundsBar"; import LoginPage from "./pages/Login"; import PlanPage from "./pages/Plan"; import TradesPage from "./pages/Trades"; @@ -63,6 +64,7 @@ function Shell({ children }: { children: ReactNode }) { +
{children}