本策略针对加密货币市场中两种黄金挂钩代币(PAXG 与 XAUT)之间长期存在的价格联动关系,运用统计套利原理,捕捉两者价差偏离均值时的回归机会。策略以秒级频率轮询行情,以分钟级K线作为统计样本,属于低频、中性的市场中性策略。
策略持续采集两个合约的分钟K线收盘价,计算实时价差序列(以百分比表示):
价差 = (价格₁ - 价格₂) / 价格₂ × 100%
在滚动统计窗口内,计算价差的均值(μ)与标准差(σ),构建动态统计区间: - 上轨 = μ + N×σ(默认 N=2) - 下轨 = μ - N×σ
| 条件 | 操作 |
|---|---|
| 当前价差 > 上轨 | 做空第一腿 / 做多第二腿 |
| 当前价差 < 下轨 | 做多第一腿 / 做空第二腿 |
开仓时记录当前均值作为目标回归价差。
价差回归至开仓时的均值水平即触发平仓,实现价差收敛利润的兑现。
为降低单腿暴露风险,策略采用严格的顺序执行逻辑: 1. 先对第一个合约下市价单,等待成交确认 2. 成交后,再对第二个合约下市价单 3. 若第二腿下单失败,自动平掉第一腿,避免裸露敞口
每笔订单设有超时机制,超时后自动撤销,防止挂单长期未成交导致的仓位错乱。
策略实时输出以下面板信息: - 两个合约的当前价格与更新时间 - 价差统计指标(均值、标准差、上下轨、Z-Score) - 当前持仓状态(方向、开仓价差、目标价差、浮动盈亏、持仓时长) - 历史交易记录与胜率统计 - 账户权益变化与累计盈亏曲线 - 价差历史分布(最小值、中位数、最大值、波动范围)
/*backtest
start: 2026-01-28 00:00:00
end: 2026-02-01 16:08:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_Bybit","currency":"XAUT_USDT","balance":500000}]
*/
/*
黄金代币统计套利策略
===========================
- 统计套利:均值±2σ开仓,回归均值平仓
- 低频策略:使用分钟K线数据
- 顺序下单:先开第一腿,成功后开第二腿
- 市价单:使用-1市价单
*/
// ==================== 参数 ====================
var Symbol1 = "PAXG_USDT.swap"; // 第一个交易对
var Symbol2 = "XAUT_USDT.swap"; // 第二个交易对
var StatPeriod = 60; // 统计周期(分钟K线数量)
var StdMultiple = 2; // 标准差倍数
var TradeAmount = 1; // 每次交易数量
var MinStd = 0.05; // 最小标准差(%)
var CheckInterval = 10000; // 检查间隔(毫秒)
var OrderTimeout = 30000; // 订单超时(毫秒)
// ==================== 工具函数 ====================
var $ = {
now: Date.now,
n: function(v, p) {
return v == null || isNaN(v) ? '-' : parseFloat(v).toFixed(p || 4);
},
time: function(ts) {
var d = new Date(ts);
return ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2) + ':' + ('0'+d.getSeconds()).slice(-2);
},
duration: function(ms) {
var s = ms/1000|0, m = s/60|0, h = m/60|0;
return h > 0 ? h+'时'+(m%60)+'分' : m > 0 ? m+'分'+(s%60)+'秒' : s+'秒';
},
color: function(v, p) {
var s = $.n(v, p);
return v > 0 ? '🟢+'+s : v < 0 ? '🔴'+s : s;
}
};
// ==================== 全局状态 ====================
var S = {
start: 0,
lastSave: 0,
lastCheck: 0,
// K线数据缓存
records1: [],
records2: [],
// 统计数据
stat: {
mean: 0,
std: 0,
upper: 0,
lower: 0,
count: 0,
spreads: []
},
// 当前价格
price1: 0,
price2: 0,
currentSpread: 0,
// 持仓状态
position: {
active: false,
direction: 0, // 1: 空Symbol1/多Symbol2, -1: 多Symbol1/空Symbol2
openSpread: 0,
targetSpread: 0, // 目标回归价差(开仓时的均值)
openTime: 0,
openEquity: 0,
amount: 0
},
// 交易统计
trades: [],
totalPnL: 0,
winCount: 0,
lossCount: 0,
// 账户
equity: {
current: 0,
initial: 0
}
};
// ==================== K线和价差计算 ====================
var dataManager = {
// 获取K线数据
fetchRecords: function() {
var records1 = exchange.GetRecords(Symbol1,PERIOD_M1);
var records2 = exchange.GetRecords(Symbol2,PERIOD_M1);
if (!records1 || !records2 || records1.length < StatPeriod || records2.length < StatPeriod) {
return false;
}
S.records1 = records1.slice(-StatPeriod);
S.records2 = records2.slice(-StatPeriod);
// 更新当前价格
S.price1 = records1[records1.length - 1].Close;
S.price2 = records2[records2.length - 1].Close;
S.currentSpread = (S.price1 - S.price2) / S.price2 * 100;
return true;
},
// 计算价差序列
calcSpreadSeries: function() {
var spreads = [];
var len = Math.min(S.records1.length, S.records2.length);
for (var i = 0; i < len; i++) {
var p1 = S.records1[i].Close;
var p2 = S.records2[i].Close;
if (p2 > 0) {
spreads.push((p1 - p2) / p2 * 100);
}
}
return spreads;
},
// 计算统计指标
calcStatistics: function() {
var spreads = this.calcSpreadSeries();
if (spreads.length < StatPeriod) {
return false;
}
// 计算均值
var sum = 0;
for (var i = 0; i < spreads.length; i++) {
sum += spreads[i];
}
var mean = sum / spreads.length;
// 计算标准差
var sqSum = 0;
for (var i = 0; i < spreads.length; i++) {
sqSum += Math.pow(spreads[i] - mean, 2);
}
var std = Math.sqrt(sqSum / spreads.length);
S.stat.mean = mean;
S.stat.std = std;
S.stat.upper = mean + StdMultiple * std;
S.stat.lower = mean - StdMultiple * std;
S.stat.count = spreads.length;
S.stat.spreads = spreads;
return true;
}
};
// ==================== 订单管理 ====================
var orderManager = {
// 等待订单成交
waitOrderFilled: function(orderId, timeout) {
var startTime = $.now();
while ($.now() - startTime < timeout) {
try {
var order = exchange.GetOrder(orderId);
if (!order) {
Sleep(500);
continue;
}
// 状态: 0=未成交, 1=已成交, 2=已撤销
if (order.Status === 1) {
return { success: true, order: order };
}
if (order.Status === 2) {
return { success: false, reason: '订单已撤销' };
}
Sleep(500);
} catch(e) {
Sleep(500);
}
}
// 超时,尝试撤单
try {
exchange.CancelOrder(orderId);
} catch(e) {}
return { success: false, reason: '订单超时' };
},
// 下单并等待成交(市价单)
placeAndWait: function(symbol, side, amount) {
try {
var orderId = exchange.CreateOrder(symbol, side, -1, amount);
if (!orderId) {
return { success: false, reason: '下单失败' };
}
Log("📤 下单: " + symbol + " " + side + " " + amount + " | 订单ID: " + orderId);
return this.waitOrderFilled(orderId, OrderTimeout);
} catch(e) {
return { success: false, reason: e.message };
}
},
// 顺序开双腿
openPair: function(direction) {
var side1, side2;
if (direction === 1) {
// 空Symbol1,多Symbol2
side1 = "sell";
side2 = "buy";
} else {
// 多Symbol1,空Symbol2
side1 = "buy";
side2 = "sell";
}
Log("════════════════════════════════════");
Log("📊 开仓 | " + (direction === 1 ? "空"+Symbol1+"/多"+Symbol2 : "多"+Symbol1+"/空"+Symbol2));
// 第一腿
var result1 = this.placeAndWait(Symbol1, side1, TradeAmount);
if (!result1.success) {
Log("❌ 第一腿开仓失败: " + result1.reason);
Log("════════════════════════════════════");
return false;
}
Log("✅ 第一腿成交");
// 第二腿
var result2 = this.placeAndWait(Symbol2, side2, TradeAmount);
if (!result2.success) {
Log("⚠️ 第二腿开仓失败,平掉第一腿");
var closeSide1 = direction === 1 ? "closesell" : "closebuy";
this.placeAndWait(Symbol1, closeSide1, TradeAmount);
Log("════════════════════════════════════");
return false;
}
Log("✅ 第二腿成交");
Log("🎉 双腿开仓成功!");
Log("════════════════════════════════════");
return true;
},
// 顺序平双腿
closePair: function(direction) {
var closeSide1, closeSide2;
if (direction === 1) {
closeSide1 = "closesell";
closeSide2 = "closebuy";
} else {
closeSide1 = "closebuy";
closeSide2 = "closesell";
}
Log("════════════════════════════════════");
Log("📊 平仓中...");
// 平第一腿
var result1 = this.placeAndWait(Symbol1, closeSide1, S.position.amount);
if (!result1.success) {
Log("⚠️ 第一腿平仓失败: " + result1.reason + ",继续尝试第二腿");
} else {
Log("✅ 第一腿平仓成功");
}
// 平第二腿
var result2 = this.placeAndWait(Symbol2, closeSide2, S.position.amount);
if (!result2.success) {
Log("⚠️ 第二腿平仓失败: " + result2.reason);
} else {
Log("✅ 第二腿平仓成功");
}
Log("════════════════════════════════════");
return result1.success && result2.success;
}
};
// ==================== 套利核心逻辑 ====================
var arbCore = {
// 检查开仓条件
shouldOpen: function() {
if (S.stat.count < StatPeriod) {
return { open: false, reason: '样本不足(' + S.stat.count + '/' + StatPeriod + ')' };
}
if (S.stat.std < MinStd) {
return { open: false, reason: '波动太小(' + $.n(S.stat.std, 4) + '%)' };
}
if (S.currentSpread > S.stat.upper) {
return { open: true, direction: 1, reason: '超上轨' };
}
if (S.currentSpread < S.stat.lower) {
return { open: true, direction: -1, reason: '超下轨' };
}
return { open: false, reason: '区间内' };
},
// 检查平仓条件
shouldClose: function() {
if (!S.position.active) {
return { close: false };
}
var target = S.position.targetSpread;
if (S.position.direction === 1 && S.currentSpread <= target) {
return { close: true, reason: '回归均值(目标:' + $.n(target, 4) + '%)' };
}
if (S.position.direction === -1 && S.currentSpread >= target) {
return { close: true, reason: '回归均值(目标:' + $.n(target, 4) + '%)' };
}
return { close: false };
},
// Z-Score计算
zScore: function() {
if (S.stat.std <= 0) return 0;
return (S.currentSpread - S.stat.mean) / S.stat.std;
},
// 获取当前权益
getEquity: function() {
try {
var acc = exchange.GetAccount();
if (acc) {
S.equity.current = acc.Equity || acc.Balance || 0;
return S.equity.current;
}
} catch(e) {}
return S.equity.current;
},
// 开仓
open: function(direction) {
if (S.position.active) return false;
var equityBefore = this.getEquity();
Log("价差:" + $.n(S.currentSpread, 4) + "% μ:" + $.n(S.stat.mean, 4) + "% σ:" + $.n(S.stat.std, 4) + "%");
var success = orderManager.openPair(direction);
if (success) {
S.position = {
active: true,
direction: direction,
openSpread: S.currentSpread,
targetSpread: S.stat.mean, // 记录开仓时的均值作为目标
openTime: $.now(),
openEquity: equityBefore,
amount: TradeAmount
};
Log("目标回归价差: " + $.n(S.stat.mean, 4) + "%");
return true;
}
return false;
},
// 平仓
close: function(reason) {
if (!S.position.active) return false;
var pos = S.position;
Log("开仓:" + $.n(pos.openSpread, 4) + "% 当前:" + $.n(S.currentSpread, 4) + "% 目标:" + $.n(pos.targetSpread, 4) + "%");
orderManager.closePair(pos.direction);
// 计算盈亏
var equityAfter = this.getEquity();
var pnlUsd = equityAfter - pos.openEquity;
S.totalPnL += pnlUsd;
if (pnlUsd > 0) S.winCount++;
else S.lossCount++;
// 记录交易
S.trades.push({
t: $.now(),
direction: pos.direction,
openSpread: pos.openSpread,
closeSpread: S.currentSpread,
targetSpread: pos.targetSpread,
pnlUsd: pnlUsd,
reason: reason,
holdTime: $.now() - pos.openTime
});
if (S.trades.length > 100) {
S.trades = S.trades.slice(-50);
}
Log("✅ 平仓完成");
Log("盈亏: " + $.color(pnlUsd, 4) + " USD");
Log("累计: " + $.color(S.totalPnL, 4) + " USD");
// 重置持仓
S.position = {
active: false,
direction: 0,
openSpread: 0,
targetSpread: 0,
openTime: 0,
openEquity: 0,
amount: 0
};
return true;
},
// 主逻辑
tick: function() {
// 获取数据
if (!dataManager.fetchRecords()) {
return;
}
// 计算统计
if (!dataManager.calcStatistics()) {
return;
}
// 检查平仓
if (S.position.active) {
var closeCheck = this.shouldClose();
if (closeCheck.close) {
this.close(closeCheck.reason);
return;
}
}
// 检查开仓
if (!S.position.active) {
var openCheck = this.shouldOpen();
if (openCheck.open) {
this.open(openCheck.direction);
}
}
}
};
// ==================== 存储 ====================
var store = {
save: function() {
_G('position', S.position);
_G('trades', S.trades);
_G('totalPnL', S.totalPnL);
_G('winCount', S.winCount);
_G('lossCount', S.lossCount);
_G('equityInitial', S.equity.initial);
},
load: function() {
var v;
if ((v = _G('position'))) S.position = v;
if ((v = _G('trades'))) S.trades = v;
if ((v = _G('totalPnL')) != null) S.totalPnL = v;
if ((v = _G('winCount')) != null) S.winCount = v;
if ((v = _G('lossCount')) != null) S.lossCount = v;
if ((v = _G('equityInitial')) != null) S.equity.initial = v;
}
};
// ==================== 显示 ====================
function render() {
var now = $.now();
var tables = [];
// 行情表
var t1 = {
type: 'table',
title: '💹 行情',
cols: ['币种', '价格', '更新时间'],
rows: []
};
t1.rows.push([Symbol1, $.n(S.price1, 2), S.records1.length > 0 ? $.time(S.records1[S.records1.length-1].Time) : '-']);
t1.rows.push([Symbol2, $.n(S.price2, 2), S.records2.length > 0 ? $.time(S.records2[S.records2.length-1].Time) : '-']);
tables.push(t1);
// 统计表
var t2 = {
type: 'table',
title: '📊 统计套利 (' + StatPeriod + '分钟/' + StdMultiple + 'σ)',
cols: ['指标', '值', '指标', '值'],
rows: []
};
var z = arbCore.zScore();
var zIcon = Math.abs(z) >= StdMultiple ? '🔴' : Math.abs(z) >= 1 ? '🟡' : '🟢';
t2.rows.push(['当前价差', $.n(S.currentSpread, 4) + '%', 'Z-Score', zIcon + $.n(z, 2) + 'σ']);
t2.rows.push(['均值μ', $.n(S.stat.mean, 4) + '%', '标准差σ', $.n(S.stat.std, 4) + '%']);
t2.rows.push(['上轨+' + StdMultiple + 'σ', $.n(S.stat.upper, 4) + '%', '下轨-' + StdMultiple + 'σ', $.n(S.stat.lower, 4) + '%']);
var openCheck = arbCore.shouldOpen();
t2.rows.push(['样本数', S.stat.count + '/' + StatPeriod, '信号', openCheck.open ? '🟢' + openCheck.reason : '⏸️' + openCheck.reason]);
tables.push(t2);
// 持仓表
var t3 = {
type: 'table',
title: '📈 策略持仓',
cols: ['指标', '值', '指标', '值'],
rows: []
};
if (S.position.active) {
var dir = S.position.direction === 1 ? '空' + Symbol1 + '/多' + Symbol2 : '多' + Symbol1 + '/空' + Symbol2;
var unrealizedPnL = S.equity.current - S.position.openEquity;
var spreadChange = S.currentSpread - S.position.openSpread;
var toTarget = S.position.direction === 1 ?
S.currentSpread - S.position.targetSpread :
S.position.targetSpread - S.currentSpread;
t3.rows.push(['状态', '🔴持仓', '方向', dir]);
t3.rows.push(['开仓价差', $.n(S.position.openSpread, 4) + '%', '当前价差', $.n(S.currentSpread, 4) + '%']);
t3.rows.push(['目标价差', $.n(S.position.targetSpread, 4) + '%', '距目标', $.n(toTarget, 4) + '%']);
t3.rows.push(['价差变化', $.color(spreadChange, 4) + '%', '数量', S.position.amount]);
t3.rows.push(['浮动盈亏', $.color(unrealizedPnL, 4) + ' USD', '持仓时间', $.duration(now - S.position.openTime)]);
} else {
t3.rows.push(['状态', '🟢空仓', '-', '-']);
}
tables.push(t3);
// 交易统计表
var t4 = {
type: 'table',
title: '📊 交易统计',
cols: ['指标', '值', '指标', '值'],
rows: []
};
var wr = (S.winCount + S.lossCount) > 0 ? (S.winCount / (S.winCount + S.lossCount) * 100).toFixed(1) : '-';
t4.rows.push(['累计盈亏', $.color(S.totalPnL, 4) + ' USD', '胜率', wr + '%']);
t4.rows.push(['胜/负', S.winCount + '/' + S.lossCount, '交易数', S.trades.length]);
t4.rows.push(['运行时间', $.duration(now - S.start), '交易量', TradeAmount]);
tables.push(t4);
// 权益表
var t5 = {
type: 'table',
title: '💰 账户权益',
cols: ['指标', '值', '指标', '值'],
rows: []
};
var totalChange = S.equity.current - S.equity.initial;
t5.rows.push(['当前权益', $.n(S.equity.current, 4), '初始权益', $.n(S.equity.initial, 4)]);
t5.rows.push(['总变化', $.color(totalChange, 4), '-', '-']);
tables.push(t5);
// 最近交易表
if (S.trades.length > 0) {
var t6 = {
type: 'table',
title: '📝 最近交易',
cols: ['时间', '方向', '开仓', '平仓', '目标', '盈亏', '原因'],
rows: []
};
S.trades.slice(-5).reverse().forEach(function(tr) {
var dir = tr.direction === 1 ? '空P多X' : '多P空X';
t6.rows.push([
$.time(tr.t),
dir,
$.n(tr.openSpread, 4) + '%',
$.n(tr.closeSpread, 4) + '%',
$.n(tr.targetSpread, 4) + '%',
$.color(tr.pnlUsd, 4),
tr.reason
]);
});
tables.push(t6);
}
// 价差分布表
if (S.stat.spreads.length > 0) {
var t7 = {
type: 'table',
title: '📈 价差分布',
cols: ['指标', '值'],
rows: []
};
var spreads = S.stat.spreads.slice();
spreads.sort(function(a, b) { return a - b; });
var min = spreads[0];
var max = spreads[spreads.length - 1];
var median = spreads[Math.floor(spreads.length / 2)];
t7.rows.push(['最小值', $.n(min, 4) + '%']);
t7.rows.push(['中位数', $.n(median, 4) + '%']);
t7.rows.push(['最大值', $.n(max, 4) + '%']);
t7.rows.push(['波动范围', $.n(max - min, 4) + '%']);
tables.push(t7);
}
// 更新收益曲线
LogProfit(S.totalPnL, "&");
// 状态栏
var posIcon = S.position.active ? '📈' : '⏸️';
var z = arbCore.zScore();
var sigIcon = Math.abs(z) >= StdMultiple ? '🔴' : Math.abs(z) >= 1 ? '🟡' : '🟢';
LogStatus(
'🟢' + posIcon + sigIcon + ' | ' + _D() +
' | 价差:' + $.n(S.currentSpread, 3) + '%' +
' | μ:' + $.n(S.stat.mean, 3) + '% σ:' + $.n(S.stat.std, 3) + '%' +
' | 盈亏:' + $.color(S.totalPnL, 2) + '\n' +
'命令: 清除|强制平仓|刷新权益\n' +
tables.map(function(t) { return '`' + JSON.stringify(t) + '`'; }).join('\n')
);
}
// ==================== 命令处理 ====================
function handleCommand() {
var c = GetCommand();
if (!c) return;
if (c === '清除') {
_G(null);
S.stat = { mean: 0, std: 0, upper: 0, lower: 0, count: 0, spreads: [] };
S.position = { active: false, direction: 0, openSpread: 0, targetSpread: 0, openTime: 0, openEquity: 0, amount: 0 };
S.trades = [];
S.totalPnL = 0;
S.winCount = 0;
S.lossCount = 0;
S.equity.initial = 0;
Log("✅ 已清除所有数据");
}
else if (c === '强制平仓') {
if (S.position.active) {
arbCore.close('手动平仓');
} else {
Log("⚠️ 当前无持仓");
}
}
else if (c === '刷新权益') {
arbCore.getEquity();
Log("✅ 权益已刷新: " + $.n(S.equity.current, 4));
}
}
// ==================== 主函数 ====================
function main() {
LogReset(0);
SetErrorFilter("order not found|GetOrder");
Log("════════════════════════════════════");
Log("🚀 Lighter 统计套利 - 低频版本 V1.0");
Log("════════════════════════════════════");
Log("交易对: " + Symbol1 + " / " + Symbol2);
Log("统计周期: " + StatPeriod + "分钟 | 阈值: " + StdMultiple + "σ");
Log("交易量: " + TradeAmount + " | 最小σ: " + MinStd + "%");
Log("检查间隔: " + (CheckInterval / 1000) + "秒");
Log("════════════════════════════════════");
S.start = $.now();
store.load();
// 初始化权益
S.equity.current = arbCore.getEquity();
if (S.equity.initial === 0) {
S.equity.initial = S.equity.current;
}
Log("初始权益: " + $.n(S.equity.initial, 4));
if (S.position.active) {
Log("📈 恢复持仓: " + (S.position.direction === 1 ? "空" + Symbol1 + "/多" + Symbol2 : "多" + Symbol1 + "/空" + Symbol2));
Log("目标回归价差: " + $.n(S.position.targetSpread, 4) + "%");
}
Log("════════════════════════════════════");
while (true) {
try {
var now = $.now();
// 按间隔检查
if (now - S.lastCheck >= CheckInterval) {
S.lastCheck = now;
// 刷新权益
arbCore.getEquity();
// 执行套利逻辑
arbCore.tick();
}
// 处理命令
handleCommand();
// 更新显示
render();
// 定期保存
if (now - S.lastSave > 60000) {
S.lastSave = now;
store.save();
}
} catch(e) {
Log("❌ 错误: " + e.message);
}
Sleep(1000);
}
}
function onexit() {
store.save();
Log("👋 策略退出,数据已保存");
}