本策略是基于风险平价(Risk Parity)理论构建的多资产量化组合管理系统,运行于发明者量化(FMZ)平台,支持实盘与模拟双模式切换,适合追求风险均衡分散而非收益最大化的中长期配置需求。
| 资产标签 | 合约代码 | 资产类别 |
|---|---|---|
| BTC | BTC_USDT.swap | 加密货币 |
| XAU | XAU_USDT.swap | 贵金属(黄金) |
| SPY | SPY_USDT.swap | 美股指数 |
| OIL | CL_USDT.swap | 大宗商品(原油) |
四类资产横跨股票、商品、贵金属、加密货币,天然具备低相关性基础,是风险平价策略的理想标的组合。
核心目标:使每个资产对组合总风险的边际贡献相等,而非等额资金分配。
\[RC_i = w_i \cdot \frac{(\Sigma w)_i}{\sigma_p} = \frac{\sigma_p}{N}, \quad \forall i\]
使用指数加权移动平均(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}}\]
策略支持做多与做空,通过计算每个资产与等波动率参考组合的协方差符号判断方向:
\[\text{CovWithPortfolio}_i = \sum_j w_j^{eqvol} \cdot \Sigma_{ij}\]
单一资产空头权重上限为0.5,防止极端空头仓位集中。
根据组合实际年化波动率动态计算杠杆倍数,使组合波动率贴近目标值:
\[\text{Leverage} = \min\left(\frac{\sigma_{target}}{\sigma_{portfolio} \cdot \sqrt{8760}},\ \text{MaxLeverage}\right)\]
| 触发条件 | 说明 |
|---|---|
| 首次运行 | 初始建仓 |
| 定时触发 | 默认每24小时再平衡一次 |
| 权重漂移 | 任一资产权重相对偏差超过15%触发 |
| 方向翻转 | 任一资产多空方向发生切换时立即触发 |
| 手动触发 | 通过控制台按钮立即执行 |
根据组合实时波动率自动调整策略轮询频率:
| 年化波动率 | 轮询间隔 |
|---|---|
| > 50% | 15分钟(高频监控) |
| 30% ~ 50% | 30分钟(中频监控) |
| < 30% | 60分钟(低频监控) |
主循环(自适应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 | 最小调仓金额 |
策略提供完整的可视化控制台,包含:
/*
* ============================================================================
* 大类资产风险平价策略 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);
}
}