RWA大类资产风险平价策略


创建日期: 2026-04-07 14:28:16 最后修改: 2026-04-14 10:09:45
复制: 8 点击次数: 87
avatar of ianzeng123 ianzeng123
2
关注
449
关注者

一、策略定位

本策略是基于风险平价(Risk Parity)理论构建的多资产量化组合管理系统,运行于发明者量化(FMZ)平台,支持实盘与模拟双模式切换,适合追求风险均衡分散而非收益最大化的中长期配置需求。


二、资产池构成

资产标签 合约代码 资产类别
BTC BTC_USDT.swap 加密货币
XAU XAU_USDT.swap 贵金属(黄金)
SPY SPY_USDT.swap 美股指数
OIL CL_USDT.swap 大宗商品(原油)

四类资产横跨股票、商品、贵金属、加密货币,天然具备低相关性基础,是风险平价策略的理想标的组合。


三、核心策略逻辑

3.1 风险平价权重求解

核心目标:使每个资产对组合总风险的边际贡献相等,而非等额资金分配。

\[RC_i = w_i \cdot \frac{(\Sigma w)_i}{\sigma_p} = \frac{\sigma_p}{N}, \quad \forall i\]

  • 采用数值迭代求解,最多迭代2000次,收敛精度 \(< 10^{-12}\)
  • 风险贡献平衡意味着:波动率高的资产自动获得更低权重,波动率低的资产获得更高权重

3.2 EWMA协方差矩阵

使用指数加权移动平均(EWMA)估计协方差矩阵,赋予近期数据更高权重:

\[\hat{\sigma}_{ij} = \frac{\sum_{t} \lambda^{T-1-t}(r_{i,t}-\bar{r}_i)(r_{j,t}-\bar{r}_j)}{\sum_t \lambda^{T-1-t}}\]

  • 默认衰减因子 \(\lambda = 0.94\),符合 RiskMetrics 行业标准
  • 基于15分钟K线对数收益率,默认回看60根K线
  • 协方差矩阵进行轻度正则化,保证数值稳定性

3.3 多空双向持仓

策略支持做多与做空,通过计算每个资产与等波动率参考组合的协方差符号判断方向:

\[\text{CovWithPortfolio}_i = \sum_j w_j^{eqvol} \cdot \Sigma_{ij}\]

  • 结果为负:资产与组合反向运动 → 开空
  • 结果为正:资产与组合同向运动 → 开多

单一资产空头权重上限为0.5,防止极端空头仓位集中。

3.4 杠杆自适应调节

根据组合实际年化波动率动态计算杠杆倍数,使组合波动率贴近目标值:

\[\text{Leverage} = \min\left(\frac{\sigma_{target}}{\sigma_{portfolio} \cdot \sqrt{8760}},\ \text{MaxLeverage}\right)\]

  • 默认目标年化波动率15%,最大杠杆3倍
  • 市场平静时自动加杠杆,市场剧烈波动时自动降杠杆

四、再平衡机制

触发条件 说明
首次运行 初始建仓
定时触发 默认每24小时再平衡一次
权重漂移 任一资产权重相对偏差超过15%触发
方向翻转 任一资产多空方向发生切换时立即触发
手动触发 通过控制台按钮立即执行

五、风险控制体系

5.1 紧急风险熔断

  • 实时监控各持仓资产的不利价格变动
  • 单资产亏损方向价格变动超过5% 触发紧急减仓
  • 每次减仓50%仓位,保留剩余持仓

5.2 波动率自适应轮询

根据组合实时波动率自动调整策略轮询频率:

年化波动率 轮询间隔
> 50% 15分钟(高频监控)
30% ~ 50% 30分钟(中频监控)
< 30% 60分钟(低频监控)

5.3 资金使用率控制

  • 默认仅使用95%权益建仓,保留5%现金缓冲
  • 多资产同时加仓时按比例缩放,防止现金耗尽

六、策略运行架构

主循环(自适应15/30/60分钟)
  ├── 获取实时行情
  ├── 紧急风险检查
  ├── K线数据对齐
  ├── EWMA协方差计算
  ├── 风险平价权重求解
  ├── 判断是否触发再平衡
  ├── 执行调仓(实盘/模拟)
  └── 渲染仪表板 + 净值图表
        └── 子循环(每1分钟刷新持仓盈亏)

七、参数总览

参数 默认值 说明
TRADE_MODE paper 实盘/模拟切换
INIT_CAPITAL 10,000 USDT 模拟初始资金
LOOKBACK 60根 协方差回看窗口
EWMA_LAMBDA 0.94 EWMA衰减因子
TARGET_VOL 15% 目标年化波动率
MAX_LEVERAGE 3倍 最大杠杆上限
REBALANCE_HOURS 24小时 定时再平衡周期
DRIFT_THRESHOLD 15% 权重漂移触发阈值
POSITION_RATIO 95% 资金使用率
MIN_REBAL_USDT 5 USDT 最小调仓金额

八、操作界面功能

策略提供完整的可视化控制台,包含:

  • 账户概览:实时权益、总盈亏、收益率、可用现金、浮盈浮亏
  • 资产配置表:各资产权重、方向、名义金额、年化波动率、风险贡献占比
  • 相关系数矩阵:实时显示资产间动态相关性
  • 持仓监控:每分钟刷新浮盈、入场价、现价对比
  • 成交记录:最近10笔交易明细
  • 净值曲线:1分钟精度的账户权益折线图
  • 快捷按钮:切换模式、立即再平衡、全部平仓、重置账户、单品种手动平仓
策略源码
/*
 * ============================================================================
 *  大类资产风险平价策略 v2.5
 *  基于发明者量化(FMZ) API框架 · 支持实盘/模拟双模式
 *
 *  资产池: SPYUSDT(美股) · XAUUSDT(黄金) · CLUSDT(原油) · BTCUSDT(比特币)
 *  框架:   风险平价(Risk Parity) + EWMA协方差 + 杠杆调节 + 自动再平衡
 *  交易:   buy/closebuy(多头) + sell/closesell(空头) · 双向策略
 *  模式:   参数 TRADE_MODE = "live" 实盘 | "paper" 模拟
 *
 *  v2.5 修复清单:
 *    [Fix4] calcEquity: `_G('pt_cash') || INIT_CAPITAL` JS falsy陷阱修复
 *           当 cash=0 时,0||INIT_CAPITAL 返回 INIT_CAPITAL,导致权益虚增10000U
 *           修复:改用 null/undefined 显式判断,cash=0 时正确返回 0
 *    [Fix5] paperRebalance: 多资产同轮加仓保证金预算
 *           原逻辑先到先得,多资产同时加仓时现金被首个资产榨干
 *           修复:调仓前预算总需求保证金,按比例缩放所有资产加仓量
 *           同步修复 renderDashboard / paperCloseAll 中同类 falsy 陷阱
 *
 *  v2.4 修复清单(保留):
 *    [Fix1] calcEWMACov: 正则化系数 0.05 → 0.001,防止负相关信号被掩盖
 *    [Fix2] solveRiskParity: 做空判断从"协方差行和"改为"与等波动率组合协方差"
 *    [Fix3] solveRiskParity: 迭代内符号由预判断 signs[] 固定,不随 w 漂移
 *
 *  v2.3 变更(保留):
 *    - 持仓盈利/收益曲线每1分钟独立刷新(主循环内子循环,无多线程)
 *    - 权重允许为负,solveRiskParity 支持 long/short 双向
 *    - 实盘/模拟均支持开空(sell)、平空(closesell)
 *    - 做空保证金、浮盈计算与多头对称处理
 * ============================================================================
 */

// ==================== 轮询间隔 ====================
var FAST_INTERVAL  = 15 * 60 * 1000;
var MID_INTERVAL   = 30 * 60 * 1000;
var SLOW_INTERVAL  = 60 * 60 * 1000;
var PNL_INTERVAL   = 60 * 1000;

// ==================== 紧急风险参数 ====================
var EMERGENCY_DROP   = 0.05;
var EMERGENCY_REDUCE = 0.50;

// ==================== 全局变量 ====================
var _chart = null;

// ==================== 初始化 ====================
function initState() {
    if (_G('rp_initialized')) return;

    _G('rp_initialized',        true);
    _G('rp_tradeMode',          TRADE_MODE);
    _G('pt_cash',               INIT_CAPITAL);
    _G('pt_initCapital',        INIT_CAPITAL);
    _G('pt_positions',          JSON.stringify({}));
    _G('pt_tradeLog',           JSON.stringify([]));
    _G('pt_realizedPnl',        0);
    _G('pt_lock',               false);
    _G('rp_lastWeights',        JSON.stringify(null));
    _G('rp_lastRebalanceTime',  0);
    _G('rp_runCount',           0);
    _G('rp_lastCheckPrices',    JSON.stringify({}));
    _G('rp_emergencyCount',     0);
    _G('rp_lastEquity',         INIT_CAPITAL);

    Log('═══════════════════════════════════════════════');
    Log('  大类资产风险平价策略 v2.5 初始化完成');
    Log('  模式:', TRADE_MODE === 'live' ? '🔴 实盘交易' : '🟢 模拟交易');
    Log('  初始资金:', INIT_CAPITAL, 'USDT');
    Log('  资产池:', LABELS.join(', '));
    Log('  策略轮询: 15/30/60分钟(自适应) | 持仓刷新: 1分钟');
    Log('  再平衡: 每', REBALANCE_HOURS, '小时 | 漂移阈值:', DRIFT_THRESHOLD * 100 + '%');
    Log('  支持双向: 多头(buy/closebuy) + 空头(sell/closesell)');
    Log('  v2.5修复: cash=0 falsy陷阱 + 保证金预算缩放');
    Log('═══════════════════════════════════════════════');
}

// ==================== 数学工具 ====================
function mean(a) {
    var s = 0;
    for (var i = 0; i < a.length; i++) s += a[i];
    return s / a.length;
}

function calcLogReturns(prices) {
    var r = [];
    for (var i = 1; i < prices.length; i++) {
        var prev = prices[i - 1], cur = prices[i];
        if (!prev || !cur || prev <= 0 || cur <= 0) { r.push(0); continue; }
        r.push(Math.log(cur / prev));
    }
    return r;
}

// ==================== EWMA 协方差矩阵 ====================
/*
 * [Fix1] 正则化系数修复:0.05 → 0.001
 * eps = diagMean * 0.001 仅保证协方差矩阵数值稳定(正定),
 * 不扭曲真实的负相关结构
 */
function calcEWMACov(retMat, lambda) {
    var n = retMat.length, T = retMat[0].length;
    var means = [];
    for (var i = 0; i < n; i++) means.push(mean(retMat[i]));

    var cov = [];
    for (var i = 0; i < n; i++) cov.push(new Array(n).fill(0));

    for (var i = 0; i < n; i++) {
        for (var j = i; j < n; j++) {
            var c = 0, ws = 0;
            for (var t = T - 1; t >= 0; t--) {
                var w = Math.pow(lambda, T - 1 - t);
                c  += w * (retMat[i][t] - means[i]) * (retMat[j][t] - means[j]);
                ws += w;
            }
            cov[i][j] = c / ws;
            cov[j][i] = cov[i][j];
        }
    }

    // [Fix1] 正则化系数从 0.05 降至 0.001
    var diagMean = 0;
    for (var i = 0; i < n; i++) diagMean += cov[i][i];
    diagMean /= n;
    var eps = diagMean * 0.001;
    for (var i = 0; i < n; i++) cov[i][i] += eps;

    return cov;
}

function calcCorrMatrix(cov) {
    var n = cov.length, cor = [];
    for (var i = 0; i < n; i++) {
        cor.push([]);
        for (var j = 0; j < n; j++) {
            var d = Math.sqrt(cov[i][i] * cov[j][j]);
            cor[i].push(d > 0 ? cov[i][j] / d : 0);
        }
    }
    return cor;
}

// ==================== 风险平价求解(支持负权重做空)====================
function matVecMul(M, v) {
    var r = [];
    for (var i = 0; i < M.length; i++) {
        var s = 0;
        for (var j = 0; j < v.length; j++) s += M[i][j] * v[j];
        r.push(s);
    }
    return r;
}

function portVol(w, cov) {
    var sv = matVecMul(cov, w);
    var v  = 0;
    for (var i = 0; i < w.length; i++) v += w[i] * sv[i];
    if (!isFinite(v) || v < 0) return 1e-8;
    return Math.sqrt(v);
}

function riskContribs(w, cov) {
    var sv  = matVecMul(cov, w);
    var pv  = portVol(w, cov);
    var trc = [];
    if (!isFinite(pv) || pv < 1e-16) {
        for (var i = 0; i < w.length; i++) trc.push(1 / w.length);
        return trc;
    }
    for (var i = 0; i < w.length; i++) trc.push(w[i] * sv[i] / pv);
    return trc;
}

/*
 * ══════════════════════════════════════════════════════════════════
 * solveRiskParity v2.4: [Fix2] + [Fix3]
 *
 * [Fix2] 使用等波动率组合协方差判断做空方向
 *   covWithPortfolio_i = Σ_j (w_j_eqvol × Cov[i][j])
 *   < 0 → 资产与组合反向 → 做空
 *
 * [Fix3] 迭代符号由 signs[] 固定,不随 w 漂移
 * ══════════════════════════════════════════════════════════════════
 */
function solveRiskParity(cov) {
    var n = cov.length;

    // 自适应缩放
    var diagMean = 0;
    for (var i = 0; i < n; i++) diagMean += cov[i][i];
    diagMean /= n;
    var scale = (diagMean > 0 && isFinite(diagMean)) ? (1e-2 / diagMean) : 1;

    var scaledCov = [];
    for (var i = 0; i < n; i++) {
        scaledCov.push([]);
        for (var j = 0; j < n; j++) scaledCov[i].push(cov[i][j] * scale);
    }

    // [Fix2] 等波动率权重
    var invVols = [], sumInvVol = 0;
    for (var i = 0; i < n; i++) {
        var vol = Math.sqrt(Math.max(scaledCov[i][i], 1e-16));
        invVols.push(1 / vol);
        sumInvVol += 1 / vol;
    }
    var eqVolWeights = [];
    for (var i = 0; i < n; i++) {
        eqVolWeights.push(invVols[i] / sumInvVol);
    }

    // [Fix2] 判断做空方向
    var signs = [];
    for (var i = 0; i < n; i++) {
        var covWithPortfolio = 0;
        for (var j = 0; j < n; j++) {
            covWithPortfolio += eqVolWeights[j] * scaledCov[i][j];
        }
        var dir = covWithPortfolio < 0 ? -1 : 1;
        signs.push(dir);
        Log('[方向判断]', LABELS[i],
            '与等波动率组合协方差:', _N(covWithPortfolio, 8),
            '→', dir < 0 ? '🔴 做空(short)' : '🟢 做多(long)');
    }

    // 初始权重:等波动率 × 正确符号
    var w = [], sw = 0;
    for (var i = 0; i < n; i++) {
        var vol = Math.sqrt(Math.max(scaledCov[i][i], 1e-16));
        var v   = signs[i] / vol;
        w.push(v);
        sw += Math.abs(v);
    }
    for (var i = 0; i < n; i++) w[i] /= sw;

    var maxShort = typeof MAX_SHORT_WEIGHT !== 'undefined' ? MAX_SHORT_WEIGHT : 0.5;

    // 迭代求解
    for (var iter = 0; iter < 2000; iter++) {
        var trc    = riskContribs(w, scaledCov);
        var pv     = portVol(w, scaledCov);
        var target = pv / n;

        var obj = 0;
        for (var i = 0; i < n; i++) {
            for (var j = i + 1; j < n; j++) {
                var d = Math.abs(trc[i]) - Math.abs(trc[j]);
                obj  += d * d;
            }
        }
        if (obj < 1e-12) break;

        var nw = [], ns = 0;
        for (var i = 0; i < n; i++) {
            var rc = Math.abs(trc[i]);
            if (rc < 1e-16) rc = 1e-16;

            // [Fix3] 使用预判断的固定符号
            var sign = signs[i];

            var a = target / rc;
            var v = sign * Math.max(Math.abs(w[i]) * Math.pow(a, 0.5), 1e-6);
            if (!isFinite(v)) v = sign * 1e-6;

            v = Math.max(Math.min(v, maxShort), -maxShort);
            nw.push(v);
            ns += Math.abs(v);
        }

        if (!isFinite(ns) || ns <= 0) break;
        for (var i = 0; i < n; i++) w[i] = nw[i] / ns;

        var wOk = true;
        for (var i = 0; i < n; i++) {
            if (!isFinite(w[i])) { wOk = false; break; }
        }
        if (!wOk) {
            Log('⚠️ 权重数值异常,降级为等权多头');
            var eq = [];
            for (var k = 0; k < n; k++) eq.push(1 / n);
            return eq;
        }
    }

    return w;
}

function calcLeverage(w, cov) {
    var pv = portVol(w, cov) * Math.sqrt(8760);
    if (!isFinite(pv) || pv <= 0) return 1;
    return Math.min(TARGET_VOL / pv, MAX_LEVERAGE);
}

// ==================== 波动率自适应轮询间隔 ====================
function getAdaptiveSleep(w, cov) {
    if (!w || !cov) return FAST_INTERVAL;
    var vol = portVol(w, cov);
    var av  = vol * Math.sqrt(8760);
    if (!isFinite(av) || isNaN(av)) return FAST_INTERVAL;
    if (av > 0.50) {
        Log('⚠️ 高波动(年化:', _N(av * 100, 2) + '%) → 15min轮询');
        return FAST_INTERVAL;
    } else if (av > 0.30) {
        Log('📊 中波动(年化:', _N(av * 100, 2) + '%) → 30min轮询');
        return MID_INTERVAL;
    } else {
        Log('✅ 低波动(年化:', _N(av * 100, 2) + '%) → 60min轮询');
        return SLOW_INTERVAL;
    }
}

// ==================== 数据获取与对齐 ====================
function alignData() {
    var timeMap = {}, allOk = true;

    for (var i = 0; i < SYMBOLS.length; i++) {
        var rec = exchange.GetRecords(SYMBOLS[i], PERIOD_H1);
        if (!rec || rec.length < LOOKBACK + 10) {
            Log(LABELS[i] + ' K线不足:', rec ? rec.length : 0);
            allOk = false;
            continue;
        }
        for (var j = 0; j < rec.length; j++) {
            var ts = rec[j].Time;
            if (!timeMap[ts]) timeMap[ts] = {};
            timeMap[ts][LABELS[i]] = rec[j].Close;
        }
    }
    if (!allOk) return null;

    var timestamps = [];
    var keys = Object.keys(timeMap).sort(function(a, b) { return a - b; });
    for (var k = 0; k < keys.length; k++) {
        var row = timeMap[keys[k]], ok = true;
        for (var i = 0; i < LABELS.length; i++) {
            if (row[LABELS[i]] === undefined) { ok = false; break; }
        }
        if (ok) timestamps.push(parseInt(keys[k]));
    }

    var closes = {};
    for (var i = 0; i < LABELS.length; i++) closes[LABELS[i]] = [];
    for (var k = 0; k < timestamps.length; k++) {
        var ts = timestamps[k];
        for (var i = 0; i < LABELS.length; i++) {
            closes[LABELS[i]].push(timeMap[ts][LABELS[i]]);
        }
    }

    Log('数据对齐:', timestamps.length, '个共同K线(1h)');
    return { timestamps: timestamps, closes: closes };
}

// ==================== 获取实时价格 ====================
function getTickers() {
    var prices = {};
    try {
        var ts = exchange.GetTickers();
        if (ts) for (var i = 0; i < ts.length; i++) prices[ts[i].Symbol] = ts[i].Last;
    } catch(e) { Log('GetTickers异常:', e.message); }
    return prices;
}

// ==================== 安全读取 pt_cash ====================
/*
 * [Fix4] 核心修复函数
 * 原写法: var cash = _G('pt_cash') || INIT_CAPITAL;
 * JS 中 0 是 falsy,当 cash 被合法设置为 0 时,|| 运算符会错误返回 INIT_CAPITAL
 * 导致权益虚增 INIT_CAPITAL(10000U)
 *
 * 修复:显式检查 null/undefined,0 视为合法值正常返回
 */
function safeGetCash() {
    var cash = _G('pt_cash');
    if (cash === null || cash === undefined) return INIT_CAPITAL;
    return cash;
}

// ==================== 计算当前权益(1分钟刷新用)====================
/*
 * [Fix4] 同步修复: 使用 safeGetCash() 替换原 || INIT_CAPITAL 写法
 */
function calcEquity(prices) {
    var ptPos  = JSON.parse(_G('pt_positions') || '{}');
    var cash   = safeGetCash();  // [Fix4] 修复 falsy 陷阱
    var unreal = 0;

    for (var sym in ptPos) {
        var pos   = ptPos[sym];
        var price = prices[sym] || pos.entryPrice;
        if (pos.side === 'long') {
            unreal += pos.margin + (price - pos.entryPrice) * pos.qty;
        } else if (pos.side === 'short') {
            unreal += pos.margin + (pos.entryPrice - price) * pos.qty;
        }
    }
    return cash + unreal;
}

// ==================== 1分钟持仓盈利刷新 ====================
function sleepWithPnlRefresh(totalSleepMs, weights, leverage, covMatrix, corrMatrix) {
    var elapsed = 0;
    while (elapsed < totalSleepMs) {
        var step = Math.min(PNL_INTERVAL, totalSleepMs - elapsed);
        Sleep(step);
        elapsed += step;

        try {
            var prices  = getTickers();
            var equity  = calcEquity(prices);
            var initCap = _G('pt_initCapital') || INIT_CAPITAL;
            var totalPnl = equity - initCap;

            _chart.add(0, [new Date().getTime(), equity]);
            LogProfit(totalPnl, '&');
            renderDashboard(weights, leverage, covMatrix, corrMatrix, prices, totalSleepMs, true);

        } catch(e) {
            Log('持仓刷新异常:', e.message);
        }
    }
}

// ==================== 紧急风险检查 ====================
function checkEmergencyRisk(prices) {
    var mode           = _G('rp_tradeMode') || 'paper';
    var lastPrices     = JSON.parse(_G('rp_lastCheckPrices') || '{}');
    var emergencyCount = _G('rp_emergencyCount') || 0;
    var triggered      = false;

    for (var i = 0; i < SYMBOLS.length; i++) {
        var sym   = SYMBOLS[i];
        var label = LABELS[i];
        var cur   = prices[sym];
        var last  = lastPrices[sym];
        if (!cur || !last) continue;

        var ptPos = JSON.parse(_G('pt_positions') || '{}');
        var pos   = ptPos[sym];
        if (!pos) continue;

        var drop = 0;
        if (pos.side === 'long') {
            drop = (last - cur) / last;
        } else if (pos.side === 'short') {
            drop = (cur - last) / last;
        }

        if (drop >= EMERGENCY_DROP) {
            Log('🚨 紧急风险触发! [' + label + '][' + pos.side + '] 不利变动:',
                _N(drop * 100, 2) + '%', '上次:', _N(last, 4), '→ 现价:', _N(cur, 4));

            if (mode === 'live') emergencyReduceLive(sym, label, pos.side, EMERGENCY_REDUCE);
            else                 emergencyReducePaper(sym, label, cur, pos.side, EMERGENCY_REDUCE);

            emergencyCount++;
            triggered = true;
        }
    }

    if (!triggered) {
        var newLast = {};
        for (var i = 0; i < SYMBOLS.length; i++) {
            if (prices[SYMBOLS[i]]) newLast[SYMBOLS[i]] = prices[SYMBOLS[i]];
        }
        _G('rp_lastCheckPrices', JSON.stringify(newLast));
    }
    _G('rp_emergencyCount', emergencyCount);
}

// ==================== 实盘紧急减仓 ====================
function emergencyReduceLive(sym, label, side, ratio) {
    try {
        var pos = exchange.GetPositions(sym);
        if (!pos) return;
        for (var p = 0; p < pos.length; p++) {
            if (side === 'long' && (pos[p].Type === PD_LONG || pos[p].Type === 0) && pos[p].Amount > 0) {
                var qty = pos[p].Amount * ratio;
                Log(label, '🚨 紧急多头减仓', _N(ratio * 100, 0) + '%');
                exchange.CreateOrder(sym, "closebuy", -1, qty);
            } else if (side === 'short' && (pos[p].Type === PD_SHORT || pos[p].Type === 1) && pos[p].Amount > 0) {
                var qty = pos[p].Amount * ratio;
                Log(label, '🚨 紧急空头减仓', _N(ratio * 100, 0) + '%');
                exchange.CreateOrder(sym, "closesell", -1, qty);
            }
        }
    } catch(e) { Log(label, '紧急减仓异常:', e.message); }
}

// ==================== 模拟紧急减仓 ====================
function emergencyReducePaper(sym, label, price, side, ratio) {
    var ptPos    = JSON.parse(_G('pt_positions') || '{}');
    var cash     = safeGetCash();  // [Fix4]
    var realized = _G('pt_realizedPnl') || 0;
    var tradeLog = JSON.parse(_G('pt_tradeLog') || '[]');

    var pos = ptPos[sym];
    if (!pos) return;

    var reduceQty      = pos.qty * ratio;
    var releasedMargin = pos.margin * ratio;
    var pnl = 0;

    if (side === 'long') {
        pnl = (price - pos.entryPrice) * reduceQty;
    } else {
        pnl = (pos.entryPrice - price) * reduceQty;
    }

    cash     += releasedMargin + pnl;
    realized += pnl;
    pos.qty    -= reduceQty;
    pos.margin -= releasedMargin;

    tradeLog.push({
        coin: label, side: side, action: 'emergencyReduce',
        closePrice: price, reduceQty: reduceQty,
        margin: releasedMargin, pnl: _N(pnl, 4),
        time: new Date().toISOString()
    });

    Log(label, '🚨 模拟紧急减仓[' + side + ']', _N(ratio * 100, 0) + '%',
        '| PnL: $', _N(pnl, 4));

    if (pos.qty < 1e-8) delete ptPos[sym];

    _G('pt_positions',   JSON.stringify(ptPos));
    _G('pt_cash',        cash);
    _G('pt_realizedPnl', realized);
    _G('pt_tradeLog',    JSON.stringify(tradeLog));
}

// ==================== 实盘调仓(双向)====================
function liveRebalance(targetAmounts, prices) {
    Log('┌─── 🔴 实盘调仓(双向) ───┐');

    for (var i = 0; i < SYMBOLS.length; i++) {
        var sym   = SYMBOLS[i];
        var label = LABELS[i];
        var price = prices[sym];
        if (!price || price <= 0) { Log(label, '⚠️ 无价格,跳过'); continue; }

        var targetAmt  = targetAmounts[i];
        var targetQty  = Math.abs(targetAmt) / price;
        var targetSide = targetAmt >= 0 ? 'long' : 'short';

        var longQty = 0, shortQty = 0;
        try {
            var pos = exchange.GetPositions(sym);
            if (pos) {
                for (var p = 0; p < pos.length; p++) {
                    if (pos[p].Type === PD_LONG  || pos[p].Type === 0) longQty  += pos[p].Amount;
                    if (pos[p].Type === PD_SHORT || pos[p].Type === 1) shortQty += pos[p].Amount;
                }
            }
        } catch(e) { Log(label, '获取持仓异常:', e.message); }

        if (targetSide === 'long' && shortQty > 0) {
            Log(label, '🔄 平空转多 | 平空:', _N(shortQty, 6));
            try { exchange.CreateOrder(sym, "closesell", -1, shortQty); Sleep(300); } catch(e) {}
            shortQty = 0;
        }
        if (targetSide === 'short' && longQty > 0) {
            Log(label, '🔄 平多转空 | 平多:', _N(longQty, 6));
            try { exchange.CreateOrder(sym, "closebuy", -1, longQty); Sleep(300); } catch(e) {}
            longQty = 0;
        }

        var currentQty = targetSide === 'long' ? longQty : shortQty;
        var diffQty    = targetQty - currentQty;
        var diffUsdt   = Math.abs(diffQty * price);

        if (diffUsdt < MIN_REBAL_USDT) {
            Log(label, '[' + targetSide + '] 偏差', _N(diffUsdt, 2), 'USDT < 阈值,跳过');
            continue;
        }

        if (diffQty > 0) {
            if (targetSide === 'long') {
                Log(label, '📈 加多仓 |', _N(diffQty, 6), '个 |', _N(diffUsdt, 2), 'USDT');
                try {
                    var oid = exchange.CreateOrder(sym, "buy", -1, diffQty);
                    Log(label, oid ? ('✅ 开多 orderId:' + oid) : '❌ 开多失败');
                } catch(e) { Log(label, '开多异常:', e.message); }
            } else {
                Log(label, '📉 加空仓 |', _N(diffQty, 6), '个 |', _N(diffUsdt, 2), 'USDT');
                try {
                    var oid = exchange.CreateOrder(sym, "sell", -1, diffQty);
                    Log(label, oid ? ('✅ 开空 orderId:' + oid) : '❌ 开空失败');
                } catch(e) { Log(label, '开空异常:', e.message); }
            }
        } else if (diffQty < 0) {
            var closeQty = Math.min(Math.abs(diffQty), currentQty);
            if (closeQty <= 0) continue;
            if (targetSide === 'long') {
                Log(label, '📉 减多仓 |', _N(closeQty, 6), '个');
                try { exchange.CreateOrder(sym, "closebuy", -1, closeQty); } catch(e) {}
            } else {
                Log(label, '📈 减空仓 |', _N(closeQty, 6), '个');
                try { exchange.CreateOrder(sym, "closesell", -1, closeQty); } catch(e) {}
            }
        }
        Sleep(500);
    }
    Log('└─── 实盘调仓完成 ───┘');
}

function liveCloseAll() {
    Log('┌─── 🔴 实盘全部平仓 ───┐');
    for (var i = 0; i < SYMBOLS.length; i++) {
        var sym = SYMBOLS[i], label = LABELS[i];
        try {
            var pos = exchange.GetPositions(sym);
            if (!pos) continue;
            for (var p = 0; p < pos.length; p++) {
                if ((pos[p].Type === PD_LONG  || pos[p].Type === 0) && pos[p].Amount > 0) {
                    Log(label, '平多:', pos[p].Amount);
                    exchange.CreateOrder(sym, "closebuy", -1, pos[p].Amount);
                }
                if ((pos[p].Type === PD_SHORT || pos[p].Type === 1) && pos[p].Amount > 0) {
                    Log(label, '平空:', pos[p].Amount);
                    exchange.CreateOrder(sym, "closesell", -1, pos[p].Amount);
                }
            }
        } catch(e) { Log(label, '平仓异常:', e.message); }
        Sleep(300);
    }
    Log('└─── 全部平仓完成 ───┘');
}

// ==================== 模拟调仓(双向)====================
/*
 * [Fix5] 保证金预算缩放
 *
 * 原问题:
 *   多资产同轮加仓时,先到先得消耗现金
 *   cash=2000U,三个资产各需 1000U 保证金,合计 3000U
 *   第一个资产消耗 1000U → cash=1000U
 *   第二个资产消耗 1000U → cash=0U
 *   第三个资产: cash < addMarg → addMarg=cash=0 → 跳过
 *   最终: cash 精确归零,第三资产无法建仓
 *
 * 修复逻辑:
 *   1. 调仓前预算所有资产需要的总保证金 totalMarginNeeded
 *   2. 若 cash < totalMarginNeeded,计算缩放比 marginScale = cash / totalMarginNeeded
 *   3. 每个资产的目标名义金额乘以 marginScale,等比缩小所有仓位
 *   4. 确保现金永远不会归零(保留小缓冲 1U)
 */
function paperRebalance(targetAmounts, prices, leverage) {
    _G('pt_lock', true);
    Log('┌─── 🟢 模拟调仓(双向) ───┐');

    try {
        var ptPos    = JSON.parse(_G('pt_positions') || '{}');
        var cash     = safeGetCash();  // [Fix4] 修复 falsy 陷阱
        var realized = _G('pt_realizedPnl') || 0;
        var tradeLog = JSON.parse(_G('pt_tradeLog') || '[]');

        // ── [Fix5] 第一步:方向切换平仓,先回收保证金 ──
        // 方向切换会释放旧仓保证金+盈亏,需先结算才能正确预算新仓
        for (var i = 0; i < SYMBOLS.length; i++) {
            var sym        = SYMBOLS[i];
            var label      = LABELS[i];
            var price      = prices[sym];
            if (!price || price <= 0) continue;

            var targetAmt  = targetAmounts[i];
            var targetSide = targetAmt >= 0 ? 'long' : 'short';
            var targetNom  = Math.abs(targetAmt);
            var currentPos = ptPos[sym];

            // 方向切换:先平反向仓,释放保证金
            if (currentPos && currentPos.side !== targetSide && targetNom > 0) {
                var closePnl = 0;
                if (currentPos.side === 'long') {
                    closePnl = (price - currentPos.entryPrice) * currentPos.qty;
                } else {
                    closePnl = (currentPos.entryPrice - price) * currentPos.qty;
                }
                cash     += currentPos.margin + closePnl;
                realized += closePnl;
                tradeLog.push({
                    coin: label, side: currentPos.side, action: 'switchClose',
                    closePrice: price, qty: currentPos.qty,
                    margin: currentPos.margin, pnl: _N(closePnl, 4),
                    time: new Date().toISOString()
                });
                Log(label, '🔄 方向切换 平' + (currentPos.side === 'long' ? '多' : '空'),
                    '| PnL: $', _N(closePnl, 4));
                delete ptPos[sym];
            }

            // 目标金额为0:平仓,回收保证金
            if (targetNom < MIN_REBAL_USDT && ptPos[sym]) {
                var closePos = ptPos[sym];
                var closePnl = 0;
                if (closePos.side === 'long') {
                    closePnl = (price - closePos.entryPrice) * closePos.qty;
                } else {
                    closePnl = (closePos.entryPrice - price) * closePos.qty;
                }
                cash     += closePos.margin + closePnl;
                realized += closePnl;
                tradeLog.push({
                    coin: label, side: closePos.side, action: 'close',
                    closePrice: price, qty: closePos.qty,
                    margin: closePos.margin, pnl: _N(closePnl, 4),
                    time: new Date().toISOString()
                });
                Log(label, '📤 平仓 | PnL: $', _N(closePnl, 4));
                delete ptPos[sym];
            }
        }

        // ── [Fix5] 第二步:预算所有增量保证金需求 ──
        var totalMarginNeeded = 0;
        for (var i = 0; i < SYMBOLS.length; i++) {
            var sym       = SYMBOLS[i];
            var price     = prices[sym];
            if (!price || price <= 0) continue;

            var targetAmt = targetAmounts[i];
            var targetNom = Math.abs(targetAmt);
            if (targetNom < MIN_REBAL_USDT) continue;

            var currentPos = ptPos[sym];
            if (!currentPos) {
                // 新开仓:全额保证金
                totalMarginNeeded += targetNom / leverage;
            } else {
                // 同方向调仓:仅增量保证金
                var currentNom = currentPos.qty * currentPos.entryPrice;
                var diff       = targetNom - currentNom;
                if (diff > 0) {
                    totalMarginNeeded += diff / leverage;
                }
                // diff <= 0(减仓)不需要额外现金,反而释放保证金
            }
        }

        // ── [Fix5] 第三步:计算缩放比例 ──
        // 保留 1U 缓冲防止浮点误差导致现金变负
        var availableCash = Math.max(cash - 1, 0);
        var marginScale   = 1.0;
        if (totalMarginNeeded > 0 && availableCash < totalMarginNeeded) {
            marginScale = availableCash / totalMarginNeeded;
            Log('⚠️ [Fix5] 保证金预算缩放:', _N(marginScale * 100, 2) + '%',
                '| 需要:', _N(totalMarginNeeded, 2),
                '| 可用:', _N(availableCash, 2));
        }

        // ── 第四步:按缩放比例执行加仓/开仓(减仓不受影响)──
        for (var i = 0; i < SYMBOLS.length; i++) {
            var sym   = SYMBOLS[i];
            var label = LABELS[i];
            var price = prices[sym];
            if (!price || price <= 0) continue;

            var targetAmt  = targetAmounts[i];
            var targetSide = targetAmt >= 0 ? 'long' : 'short';
            var targetNom  = Math.abs(targetAmt) * marginScale;  // [Fix5] 乘以缩放比
            var targetMarg = targetNom / leverage;

            if (targetNom < MIN_REBAL_USDT) continue;

            var currentPos = ptPos[sym];

            if (!currentPos) {
                // 新开仓
                if (cash < targetMarg) {
                    Log(label, '⚠️ 现金不足 需要:', _N(targetMarg, 2), '实有:', _N(cash, 2), '跳过');
                    continue;
                }
                var qty = targetNom / price;
                cash -= targetMarg;
                ptPos[sym] = {
                    side: targetSide, entryPrice: price, qty: qty,
                    margin: targetMarg, leverage: leverage,
                    openTime: new Date().toISOString()
                };
                tradeLog.push({
                    coin: label, side: targetSide, action: 'open',
                    entryPrice: price, qty: qty, margin: targetMarg,
                    time: new Date().toISOString()
                });
                Log(label, targetSide === 'long' ? '📈' : '📉',
                    '模拟开' + (targetSide === 'long' ? '多' : '空'),
                    '| 价格:', _N(price, 4), '| 数量:', _N(qty, 6),
                    '| 保证金:', _N(targetMarg, 2));

            } else {
                var currentNom = currentPos.qty * currentPos.entryPrice;
                var diff       = targetNom - currentNom;

                if (Math.abs(diff) < MIN_REBAL_USDT) continue;

                if (diff > 0) {
                    // 加仓
                    var addMarg = diff / leverage;
                    // 安全保护:防止极端情况现金仍不足
                    if (cash < addMarg) {
                        addMarg = Math.max(cash - 1, 0);
                        diff    = addMarg * leverage;
                    }
                    if (addMarg < 1) continue;
                    var addQty = diff / price;
                    var oldVal = currentPos.qty * currentPos.entryPrice;
                    var newVal = addQty * price;
                    currentPos.entryPrice = (oldVal + newVal) / (currentPos.qty + addQty);
                    currentPos.qty    += addQty;
                    currentPos.margin += addMarg;
                    cash -= addMarg;
                    tradeLog.push({
                        coin: label, side: targetSide, action: 'add',
                        price: price, addQty: addQty, margin: addMarg,
                        time: new Date().toISOString()
                    });
                    Log(label, '加仓[' + targetSide + '] +', _N(addQty, 6),
                        '| 新均价:', _N(currentPos.entryPrice, 4),
                        '| 剩余现金:', _N(cash, 2));
                } else {
                    // 减仓(释放保证金,不受 marginScale 限制)
                    var reduceNom      = Math.abs(diff);
                    var reduceQty      = Math.min(reduceNom / price, currentPos.qty);
                    var ratio          = reduceQty / currentPos.qty;
                    var releasedMargin = currentPos.margin * ratio;
                    var pnl = 0;
                    if (currentPos.side === 'long') {
                        pnl = (price - currentPos.entryPrice) * reduceQty;
                    } else {
                        pnl = (currentPos.entryPrice - price) * reduceQty;
                    }
                    cash              += releasedMargin + pnl;
                    realized          += pnl;
                    currentPos.qty    -= reduceQty;
                    currentPos.margin -= releasedMargin;
                    tradeLog.push({
                        coin: label, side: currentPos.side, action: 'reduce',
                        closePrice: price, reduceQty: reduceQty,
                        margin: releasedMargin, pnl: _N(pnl, 4),
                        time: new Date().toISOString()
                    });
                    Log(label, '减仓[' + currentPos.side + '] -', _N(reduceQty, 6),
                        '| PnL: $', _N(pnl, 4));
                    if (currentPos.qty < 1e-8) delete ptPos[sym];
                }
            }
        }

        if (tradeLog.length > 200) tradeLog.splice(0, tradeLog.length - 200);

        _G('pt_positions',   JSON.stringify(ptPos));
        _G('pt_cash',        cash);
        _G('pt_realizedPnl', realized);
        _G('pt_tradeLog',    JSON.stringify(tradeLog));
        Log('模拟调仓完成 | 持仓:', Object.keys(ptPos).length, '个 | 现金:', _N(cash, 2));

    } finally {
        _G('pt_lock', false);
    }
    Log('└─── 模拟调仓完成 ───┘');
}

function paperCloseAll() {
    var ptPos    = JSON.parse(_G('pt_positions') || '{}');
    var cash     = safeGetCash();  // [Fix4]
    var realized = _G('pt_realizedPnl') || 0;
    var tradeLog = JSON.parse(_G('pt_tradeLog') || '[]');
    var prices   = getTickers();

    for (var sym in ptPos) {
        var pos   = ptPos[sym];
        var label = sym.replace('_USDT.swap', '');
        var price = prices[sym] || pos.entryPrice;
        var pnl   = pos.side === 'long'
                    ? (price - pos.entryPrice) * pos.qty
                    : (pos.entryPrice - price) * pos.qty;
        cash     += pos.margin + pnl;
        realized += pnl;
        tradeLog.push({
            coin: label, side: pos.side, action: 'closeAll',
            closePrice: price, qty: pos.qty,
            margin: pos.margin, pnl: _N(pnl, 4),
            time: new Date().toISOString()
        });
        Log('📤', label, '[' + pos.side + '] 全平 | PnL: $', _N(pnl, 4));
    }

    _G('pt_positions',   JSON.stringify({}));
    _G('pt_cash',        cash);
    _G('pt_realizedPnl', realized);
    _G('pt_tradeLog',    JSON.stringify(tradeLog));
    Log('🟢 全部平仓完成 | 现金:', _N(cash, 2));
}

// ==================== 交互命令处理 ====================
function handleCommand() {
    var cmd = GetCommand();
    if (!cmd) return;
    var parts = cmd.split(':');

    switch(parts[0]) {
        case '切换模式':
            var cur  = _G('rp_tradeMode') || 'paper';
            var next = cur === 'live' ? 'paper' : 'live';
            _G('rp_tradeMode', next);
            Log('🔄 交易模式切换:', cur, '→', next);
            break;

        case '全部平仓':
            var mode = _G('rp_tradeMode') || 'paper';
            if (mode === 'live') liveCloseAll();
            else                 paperCloseAll();
            _G('rp_lastWeights', JSON.stringify(null));
            break;

        case '手动平仓':
            var coin = parts[1];
            if (!coin) break;
            var sym  = coin + '_USDT.swap';
            var mode = _G('rp_tradeMode') || 'paper';
            if (mode === 'live') {
                try {
                    var pos = exchange.GetPositions(sym);
                    if (pos) {
                        for (var p = 0; p < pos.length; p++) {
                            if ((pos[p].Type === PD_LONG  || pos[p].Type === 0) && pos[p].Amount > 0)
                                exchange.CreateOrder(sym, "closebuy",  -1, pos[p].Amount);
                            if ((pos[p].Type === PD_SHORT || pos[p].Type === 1) && pos[p].Amount > 0)
                                exchange.CreateOrder(sym, "closesell", -1, pos[p].Amount);
                        }
                    }
                } catch(e) { Log('平仓异常:', e.message); }
            } else {
                var ptPos = JSON.parse(_G('pt_positions') || '{}');
                if (!ptPos[sym]) { Log(coin, '无持仓'); break; }
                var pos    = ptPos[sym];
                var prices = getTickers();
                var price  = prices[sym] || pos.entryPrice;
                var pnl    = pos.side === 'long'
                             ? (price - pos.entryPrice) * pos.qty
                             : (pos.entryPrice - price) * pos.qty;
                var cash   = safeGetCash() + pos.margin + pnl;  // [Fix4]
                _G('pt_cash', cash);
                _G('pt_realizedPnl', (_G('pt_realizedPnl') || 0) + pnl);
                delete ptPos[sym];
                _G('pt_positions', JSON.stringify(ptPos));
                var tl = JSON.parse(_G('pt_tradeLog') || '[]');
                tl.push({
                    coin: coin, side: pos.side, action: 'manualClose',
                    closePrice: price, qty: pos.qty,
                    pnl: _N(pnl, 4), time: new Date().toISOString()
                });
                _G('pt_tradeLog', JSON.stringify(tl));
                Log('✅ 模拟平仓', coin, '[' + pos.side + '] | PnL: $', _N(pnl, 4));
            }
            break;

        case '立即再平衡':
            _G('rp_lastRebalanceTime', 0);
            Log('🔁 已标记立即再平衡');
            break;

        case '重置模拟账户':
            _G('pt_cash',               INIT_CAPITAL);
            _G('pt_initCapital',        INIT_CAPITAL);
            _G('pt_positions',          JSON.stringify({}));
            _G('pt_tradeLog',           JSON.stringify([]));
            _G('pt_realizedPnl',        0);
            _G('pt_lock',               false);
            _G('rp_lastWeights',        JSON.stringify(null));
            _G('rp_lastCheckPrices',    JSON.stringify({}));
            _G('rp_emergencyCount',     0);
            Log('🔄 模拟账户已重置, 恢复', INIT_CAPITAL, 'USDT');
            break;
    }
}

// ==================== 仪表板 ====================
function renderDashboard(weights, leverage, covMatrix, corrMatrix, prices, sleepMs, isPnlRefresh) {
    var mode     = _G('rp_tradeMode') || 'paper';
    var isLive   = mode === 'live';
    var modeTag  = isLive ? '🔴 实盘交易' : '🟢 模拟交易';
    var runCount = _G('rp_runCount') || 0;
    var emgCount = _G('rp_emergencyCount') || 0;

    var initCap  = _G('pt_initCapital') || INIT_CAPITAL;
    var cash     = safeGetCash();  // [Fix4]
    var realized = _G('pt_realizedPnl') || 0;
    var ptPos    = JSON.parse(_G('pt_positions') || '{}');

    var unrealized = 0;
    for (var sym in ptPos) {
        var pos = ptPos[sym];
        var p   = prices[sym] || pos.entryPrice;
        if (pos.side === 'long') {
            unrealized += pos.margin + (p - pos.entryPrice) * pos.qty;
        } else {
            unrealized += pos.margin + (pos.entryPrice - p) * pos.qty;
        }
    }

    var equity   = cash + unrealized;
    var totalPnl = equity - initCap;
    var totalPct = totalPnl / initCap * 100;

    var intervalDesc = sleepMs === FAST_INTERVAL ? '15min(高波动)' :
                       sleepMs === MID_INTERVAL  ? '30min(中波动)' : '60min(低波动)';
    var refreshTag   = isPnlRefresh ? ' [1min盈利刷新]' : ' [策略更新]';

    var t1 = {
        type: 'table',
        title: '📊 账户概览 (' + modeTag + ' · ' + initCap + 'U 起始)' + refreshTag,
        cols: ['模式', '权益', '总盈亏', '收益率', '可用现金', '浮盈', '已实现', '紧急触发', '运行次数', '策略轮询', '操作'],
        rows: [[
            modeTag,
            '$' + _N(equity, 2),
            (totalPnl >= 0 ? '+$' : '-$') + _N(Math.abs(totalPnl), 2),
            (totalPct >= 0 ? '🟢 +' : '🔴 ') + _N(totalPct, 2) + '%',
            '$' + _N(cash, 2),
            (unrealized >= 0 ? '+$' : '-$') + _N(Math.abs(unrealized), 2),
            (realized >= 0 ? '+$' : '-$') + _N(Math.abs(realized), 2),
            '🚨 ' + emgCount + '次',
            '🔄 ' + runCount + '次',
            '⏱ ' + intervalDesc,
            [
                { type: 'button', cmd: '切换模式',    name: isLive ? '切换到模拟' : '切换到实盘' },
                { type: 'button', cmd: '立即再平衡',   name: '🔁 立即再平衡' },
                { type: 'button', cmd: '全部平仓',     name: '📤 全部平仓' },
                { type: 'button', cmd: '重置模拟账户', name: '🔄 重置模拟' }
            ]
        ]]
    };

    var t2 = {
        type: 'table',
        title: '⚖️ 风险平价资产配置 v2.5 (负权重=做空 | Fix:等波动率组合协方差判向)',
        cols: ['资产', '合约', '权重(%)', '方向', '与组合协方差', '杠杆后(%)', '名义金额', '年化波动率', '风险贡献(%)'],
        rows: []
    };
    if (weights && covMatrix) {
        var trc      = riskContribs(weights, covMatrix);
        var totalTRC = 0;
        for (var i = 0; i < trc.length; i++) totalTRC += Math.abs(trc[i]);

        var invVols2 = [], sumInvVol2 = 0;
        for (var i = 0; i < LABELS.length; i++) {
            var vol2 = Math.sqrt(Math.max(covMatrix[i][i], 1e-16));
            invVols2.push(1 / vol2);
            sumInvVol2 += 1 / vol2;
        }
        for (var i = 0; i < LABELS.length; i++) {
            var vol    = Math.sqrt(Math.abs(covMatrix[i][i])) * Math.sqrt(8760) * 100;
            var nomAmt = weights[i] * leverage * equity * POSITION_RATIO;
            var dir    = weights[i] >= 0 ? '🟢 多' : '🔴 空';
            var covWP  = 0;
            for (var j = 0; j < LABELS.length; j++) {
                covWP += (invVols2[j] / sumInvVol2) * covMatrix[i][j];
            }
            t2.rows.push([
                LABELS[i], SYMBOLS[i],
                _N(weights[i] * 100, 2),
                dir,
                _N(covWP, 8),
                _N(weights[i] * leverage * 100, 2),
                '$' + _N(Math.abs(nomAmt), 2),
                _N(vol, 2) + '%',
                _N(Math.abs(trc[i]) / totalTRC * 100, 2) + '%'
            ]);
        }
    } else {
        t2.rows.push(['等待数据...', '-', '-', '-', '-', '-', '-', '-', '-']);
    }

    var t3 = {
        type: 'table',
        title: '📐 相关系数矩阵 (EWMA λ=' + EWMA_LAMBDA + ' · v2.5修复:cash=0 falsy陷阱+保证金预算)',
        cols: [''].concat(LABELS),
        rows: []
    };
    if (corrMatrix) {
        for (var i = 0; i < LABELS.length; i++) {
            var row = [LABELS[i]];
            for (var j = 0; j < LABELS.length; j++) row.push(_N(corrMatrix[i][j], 3));
            t3.rows.push(row);
        }
    }

    var t4 = {
        type: 'table',
        title: '📋 持仓监控 · 每1分钟刷新 · ' + _D(),
        cols: ['资产', '方向', '入场价', '现价', '数量', '保证金', '浮盈%', '浮盈$', '操作'],
        rows: []
    };
    var posKeys = Object.keys(ptPos);
    if (posKeys.length > 0) {
        for (var k = 0; k < posKeys.length; k++) {
            var sym   = posKeys[k];
            var pos   = ptPos[sym];
            var label = sym.replace('_USDT.swap', '');
            var p     = prices[sym] || pos.entryPrice;
            var pnlPct, pnlAbs;
            if (pos.side === 'long') {
                pnlPct = (p - pos.entryPrice) / pos.entryPrice * 100;
                pnlAbs = (p - pos.entryPrice) * pos.qty;
            } else {
                pnlPct = (pos.entryPrice - p) / pos.entryPrice * 100;
                pnlAbs = (pos.entryPrice - p) * pos.qty;
            }
            t4.rows.push([
                label,
                pos.side === 'long' ? '🟢 多' : '🔴 空',
                _N(pos.entryPrice, 4), _N(p, 4),
                _N(pos.qty, 6), '$' + _N(pos.margin, 2),
                (pnlPct >= 0 ? '+' : '') + _N(pnlPct, 2) + '%',
                (pnlAbs >= 0 ? '+$' : '-$') + _N(Math.abs(pnlAbs), 2),
                [{ type: 'button', cmd: '手动平仓:' + label, name: '平仓' }]
            ]);
        }
    } else {
        t4.rows.push(['暂无持仓', '-', '-', '-', '-', '-', '-', '-', '-']);
    }

    var tradeLog = JSON.parse(_G('pt_tradeLog') || '[]');
    var t5 = {
        type: 'table',
        title: '📜 最近成交(最新10笔)',
        cols: ['资产', '方向', '动作', '价格', '数量', '保证金', '盈亏$', '时间'],
        rows: []
    };
    var recent = tradeLog.slice(-10).reverse();
    if (recent.length > 0) {
        for (var k = 0; k < recent.length; k++) {
            var t = recent[k];
            t5.rows.push([
                t.coin,
                t.side === 'long' ? '🟢 多' : '🔴 空',
                t.action,
                _N(t.closePrice || t.entryPrice || t.price || 0, 4),
                _N(t.qty || t.reduceQty || t.addQty || 0, 6),
                '$' + _N(t.margin || 0, 2),
                t.pnl !== undefined ? ('$' + t.pnl) : '-',
                (t.time || '').replace('T', ' ').slice(0, 19)
            ]);
        }
    } else {
        t5.rows.push(['暂无成交', '-', '-', '-', '-', '-', '-', '-']);
    }

    var lev  = leverage ? ('杠杆:' + _N(leverage, 2) + 'x | ') : '';
    var pvol = (weights && covMatrix) ?
               ('组合年化波动率:' + _N(portVol(weights, covMatrix) * Math.sqrt(8760) * 100, 2) + '% | ') : '';

    LogStatus(
        modeTag + ' | ' + lev + pvol + '持仓刷新:1min | 策略轮询:' + intervalDesc +
        ' | v2.5[Fix:cash=0+保证金预算] | ' + _D() + '\n`' +
        JSON.stringify(t1) + '`\n`' +
        JSON.stringify(t2) + '`\n`' +
        JSON.stringify(t3) + '`\n`' +
        JSON.stringify(t4) + '`\n`' +
        JSON.stringify(t5) + '`'
    );
}

// ==================== 主函数 ====================
function main() {
    LogReset(200);
    SYMBOLS = SYMBOLS.split(",");
    LABELS  = LABELS.split(",");

    initState();

    _chart = Chart({
        title: { text: '大类资产风险平价 v2.5 · 净值曲线(1min刷新)' },
        xAxis: { type: 'datetime' },
        yAxis: { title: { text: '权益(USDT)' } },
        series: [{ name: '账户权益', type: 'line', data: [] }]
    });

    var sleepMs    = FAST_INTERVAL;
    var weights    = null;
    var covMatrix  = null;
    var corrMatrix = null;
    var leverage   = null;

    while (true) {
        try {
            handleCommand();

            var runCount = (_G('rp_runCount') || 0) + 1;
            _G('rp_runCount', runCount);

            var mode   = _G('rp_tradeMode') || 'paper';
            var prices = getTickers();

            checkEmergencyRisk(prices);

            var data = alignData();
            if (!data || data.timestamps.length < LOOKBACK + 10) {
                Log('数据不足(' + (data ? data.timestamps.length : 0) + '根), 60秒后重试');
                renderDashboard(weights, leverage, covMatrix, corrMatrix, prices, sleepMs, false);
                Sleep(60000);
                continue;
            }

            // 收益率矩阵
            var retMat = [];
            for (var i = 0; i < LABELS.length; i++) {
                retMat.push(calcLogReturns(data.closes[LABELS[i]]).slice(-LOOKBACK));
            }

            // EWMA协方差 & 相关系数
            covMatrix  = calcEWMACov(retMat, EWMA_LAMBDA);
            corrMatrix = calcCorrMatrix(covMatrix);

            // 风险平价权重
            weights  = solveRiskParity(covMatrix);
            leverage = calcLeverage(weights, covMatrix);

            // 自适应轮询间隔
            sleepMs = getAdaptiveSleep(weights, covMatrix);

            // 再平衡判断
            var lastWeights = JSON.parse(_G('rp_lastWeights') || 'null');
            var lastTime    = _G('rp_lastRebalanceTime') || 0;
            var now         = new Date().getTime();
            var needRebal   = false;
            var rebalReason = '';

            if (!lastWeights) {
                needRebal   = true;
                rebalReason = '首次建仓';
            } else if ((now - lastTime) > REBALANCE_HOURS * 3600000) {
                needRebal   = true;
                rebalReason = '定时再平衡(' + REBALANCE_HOURS + 'h)';
            } else {
                for (var i = 0; i < weights.length; i++) {
                    if (lastWeights[i] !== undefined) {
                        if ((weights[i] >= 0) !== (lastWeights[i] >= 0)) {
                            needRebal   = true;
                            rebalReason = LABELS[i] + ' 方向翻转(多↔空)';
                            break;
                        }
                        if (lastWeights[i] !== 0 &&
                            Math.abs(weights[i] - lastWeights[i]) / Math.abs(lastWeights[i]) > DRIFT_THRESHOLD) {
                            needRebal   = true;
                            rebalReason = LABELS[i] + ' 权重漂移 ' +
                                          _N(Math.abs(weights[i] - lastWeights[i]) / Math.abs(lastWeights[i]) * 100, 1) + '%';
                            break;
                        }
                    }
                }
            }

            // 执行调仓
            if (needRebal) {
                Log('═══ 再平衡触发:', rebalReason, '═══');

                var totalEquity;
                if (mode === 'live') {
                    try {
                        var acc = exchange.GetAccount();
                        totalEquity = acc ? (acc.Balance + acc.FrozenBalance) : INIT_CAPITAL;
                    } catch(e) { totalEquity = INIT_CAPITAL; }
                } else {
                    totalEquity = calcEquity(prices);  // [Fix4] 统一使用修复后的 calcEquity
                }

                var targetAmounts = [];
                Log('┌─── 目标配置(v2.5) ───┐');
                for (var i = 0; i < LABELS.length; i++) {
                    var nomAmt = weights[i] * leverage * totalEquity * POSITION_RATIO;
                    targetAmounts.push(nomAmt);
                    var dir = nomAmt >= 0 ? '🟢 多' : '🔴 空';
                    Log('  ', LABELS[i], dir,
                        '| 权重:', _N(weights[i] * 100, 2) + '%',
                        '| 名义:', _N(Math.abs(nomAmt), 2), 'USDT');
                }
                Log('  杠杆:', _N(leverage, 2) + 'x | 总权益:', _N(totalEquity, 2));
                Log('└─────────────────────┘');

                if (mode === 'live') liveRebalance(targetAmounts, prices);
                else                 paperRebalance(targetAmounts, prices, leverage);

                _G('rp_lastWeights',       JSON.stringify(weights));
                _G('rp_lastRebalanceTime', now);

                var newBasePrices = {};
                for (var i = 0; i < SYMBOLS.length; i++) {
                    if (prices[SYMBOLS[i]]) newBasePrices[SYMBOLS[i]] = prices[SYMBOLS[i]];
                }
                _G('rp_lastCheckPrices', JSON.stringify(newBasePrices));
            }

            // 渲染仪表板 + 图表
            renderDashboard(weights, leverage, covMatrix, corrMatrix, prices, sleepMs, false);
            var curEquity = calcEquity(prices);
            _chart.add(0, [now, curEquity]);
            LogProfit(curEquity - (_G('pt_initCapital') || INIT_CAPITAL), '&');

        } catch(e) {
            Log('策略异常:', e.message, e.stack || '');
        }

        sleepWithPnlRefresh(sleepMs, weights, leverage, covMatrix, corrMatrix);
    }
}
全部留言
avatar of 中国叠石桥家纺城
中国叠石桥家纺城
欧易交易所可以用吗
2026-04-29 12:33:56
avatar of ianzeng123
ianzeng123
对照找一下,有没有类似的合约就可以
2026-04-29 13:02:38