1、基本源码是网友的策略:https://www.fmz.com/strategy/293325; 2、我在这个的基础上增加了联合减仓、风控等多项方案; 3、还可以增加冷静期、趋势跟踪、择时开单、防瀑布等多个方案(未更新);
该策略是一种复杂的量化交易系统,旨在通过网格交易的方式在加密货币市场(如BTC/USDT)进行多空双向操作,同时结合多种统计与金融工程方法,对市场波动性、趋势特征进行动态分析,从而实现风险控制与收益优化。策略适用于1分钟频率的期货交易,并严格控制每笔交易的最小仓位单位。
updateVol和adaptiveQuantity函数实现波动率计算与仓位调整。hurst和cusumCheck函数实现趋势判断逻辑。comboClose函数实现联合减仓逻辑,通过dynamicBand函数动态调整止损止盈价位。detectJump函数实现跳跃检测逻辑。computeES函数实现尾部风险计算。reAnchor函数实现重锚逻辑。该策略是一种高度工程化的量化交易系统,融合了现代金融工程中的多种方法论。它通过动态调整仓位、严格控制风险和利用市场波动特性,在加密货币市场中实现网格化利润捕捉。适合对量化交易与风险控制有一定了解的投资者作为参考。
/*backtest
start: 2021-01-18 00:00:00
end: 2021-12-31 23:59:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_Binance","currency":"BTC_USDT","balance":2000}]
*/
// ===== 0. 兜底 =====
const MIN_AMOUNT = 0.001;
const ES_THRESHOLD = 0.05;
const REANCHOR_MINUTE = 30;
const FORCE_REANCHOR_HOURS = 24;
function safeAmount(raw) {
let val = Number(raw);
return (isNaN(val) || val <= 0) ? MIN_AMOUNT : Math.max(val, MIN_AMOUNT);
}
function safePrice(raw) {
let val = Number(raw);
return (isNaN(val) || val <= 0) ? 1 : val;
}
// ===== 1. 波动率自适应 =====
const VOL_SPAN = 30;
let vol_history = [];
function updateVol(price) {
if (vol_history.length === 0) { vol_history.push(price); return 0.01; }
const prev = vol_history[vol_history.length - 1];
if (prev <= 0) return 0.01;
const log_ret = Math.log(price / prev);
vol_history.push(log_ret);
if (vol_history.length > VOL_SPAN) vol_history.shift();
const mean = vol_history.reduce((a, b) => a + b, 0) / vol_history.length;
const var_ = vol_history.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / vol_history.length;
return Math.sqrt(var_) || 0.01;
}
function adaptiveQuantity(baseQty, price) {
const sigma_target = 0.015;
const sigma_real = updateVol(price);
const factor = Math.min(sigma_target / sigma_real, 2.0);
return safeAmount(baseQty * factor);
}
// ===== 2. Hurst-CUSUM =====
let log_prices = [];
function hurst(prices) {
const n = prices.length;
if (n < 20) return 0.5;
const mean = prices.reduce((a, b) => a + b, 0) / n;
const y = prices.map(p => p - mean);
let z = [y[0]];
for (let i = 1; i < n; i++) z.push(z[i - 1] + y[i]);
const R = Math.max(...z) - Math.min(...z);
const S = Math.sqrt(y.reduce((a, b) => a + b * b, 0) / n);
return Math.log(R / S) / Math.log(n);
}
let cusum_pos = 0, cusum_neg = 0;
function cusumCheck(log_ret) {
const k = 0.5, h = 1.8;
cusum_pos = Math.max(0, cusum_pos + log_ret - k);
cusum_neg = Math.max(0, cusum_neg - log_ret - k);
return (cusum_pos > h || cusum_neg > h);
}
let trend_lock = false;
function trendBrake(price) {
log_prices.push(price);
if (log_prices.length > 100) log_prices.shift();
const H = hurst(log_prices.slice(-30));
if (log_prices.length < 31) return;
const log_ret = Math.log(price / log_prices[log_prices.length - 2]);
if (cusumCheck(log_ret) || H > 0.55) trend_lock = true;
if (H < 0.45) trend_lock = false;
}
// ===== 3. GARCH =====
let garch_sigma = 0.015;
function updateGarch(price) {
if (log_prices.length < 2) return garch_sigma;
const eps = Math.log(price / log_prices[log_prices.length - 2]);
const omega = 1e-6, alpha = 0.05, beta = 0.9;
garch_sigma = Math.sqrt(omega + alpha * eps * eps + beta * garch_sigma * garch_sigma) || 0.01;
return garch_sigma;
}
function dynamicBand() {
const k = 2.5, lambda = 0.6;
const sigma = updateGarch(exchange.GetTicker().Last);
return { profit: k * sigma * 100, dthrow: k * sigma * lambda * 100 };
}
// ===== 4. 跳跃检测 =====
let min1_returns = [], min5_returns = [];
function pushReturn(ret, arr, maxLen) {
arr.push(ret);
if (arr.length > maxLen) arr.shift();
}
function realizedVol(arr) {
if (arr.length < 2) return 0.01;
const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
const var_ = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length;
return Math.sqrt(var_) || 0.01;
}
function detectJump(ret, arr) {
const vol = realizedVol(arr);
if (vol === 0) return false;
const z = Math.abs(ret) / vol;
const thresh = 2.326;
return z > thresh;
}
// ===== 5. ES =====
function computeES(posList, markPrice, notional) {
const pnlArr = posList.map(p => (markPrice - p.price) * p.side * p.amount);
pnlArr.sort((a, b) => a - b);
const idx = Math.max(0, Math.floor(pnlArr.length * 0.05));
const tail = pnlArr.slice(0, idx + 1);
const es = tail.length ? tail.reduce((a, b) => a + b, 0) / tail.length : 0;
return Math.abs(es) / notional;
}
// ===== 6. 联合减仓 =====
function comboClose(markPrice, posList, side) {
const len = posList.length;
if (len < 3) return;
let combo = len <= 5 ? 3 : (len <= 10 ? 5 : 7);
combo = Math.min(combo, len);
const latest = posList.slice(-(combo - 1));
const oldest = posList.slice(0, 1);
const latestPnL = latest.reduce((s, p) => s + (markPrice - p.price) * p.side * p.amount, 0);
const oldestPnL = oldest.reduce((s, p) => s + (markPrice - p.price) * p.side * p.amount, 0);
if (latestPnL > -oldestPnL) {
latest.concat(oldest).forEach(p => {
const dir = side === LONG_SIDE ? 'closebuy' : 'closesell';
exchange.SetDirection(dir);
(dir === 'closebuy' ? exchange.Buy : exchange.Sell)(safePrice(markPrice), safeAmount(p.amount));
});
posList.splice(0, 1);
posList.splice(posList.length - (combo - 1), combo - 1);
// 关键:重新连续编号
posList.forEach((p, idx) => { /* 顺序不变,仅重新索引 */ });
if (side === LONG_SIDE) MANY_STEP = posList.length;
if (side === SHORT_SIDE) SHORT_STEP = posList.length;
}
}
// ===== 仓位记录 =====
const LONG_SIDE = 1;
const SHORT_SIDE = -1;
function addPosition(list, price, amount, side) {
list.push({ price, amount, side });
// STEP 始终等于列表长度
if (side === LONG_SIDE) MANY_STEP = list.length;
else SHORT_STEP = list.length;
}
function removePosition(list, idx) {
list.splice(idx, 1);
}
// ===== 全局变量 =====
let FIRST_BUY = true;
let MANY_BUYING = false;
let SHORT_BUYING = false;
let MANY_BUY_TIME = null;
let SHORT_BUY_TIME = null;
let MANY_EMPTY_STEP_TIME = null;
let SHORT_EMPTY_STEP_TIME = null;
let MANY_LAST_REANCHOR_TIME = 0;
let SHORT_LAST_REANCHOR_TIME = 0;
const QUANTITY = [0.001, 0.002, 0.004, 0.008, 0.016, 0.032, 0.064];
let MANY_NEXT_BUY_PRICE = 0;
let SHORT_NEXT_BUY_PRICE = 0;
let MANY_STEP = 0; // 始终等于 MANY_ORDER_LIST.length
let SHORT_STEP = 0; // 始终等于 SHORT_ORDER_LIST.length
let PROFIT_RATIO = 1;
let DOUBLE_THROW_RATIO = 1.5;
let BUY_PRICE_RATIO = 1;
let MANY_ORDER_LIST = [];
let SHORT_ORDER_LIST = [];
// ===== 下单 =====
function placeOrder(dir, price, amount) {
exchange.SetDirection(dir);
return dir === 'buy' || dir === 'closebuy'
? exchange.Buy(safePrice(price), safeAmount(amount))
: exchange.Sell(safePrice(price), safeAmount(amount));
}
// ===== 数量 =====
function getManyQty() {
return adaptiveQuantity(QUANTITY[Math.min(MANY_STEP, QUANTITY.length - 1)], exchange.GetTicker().Last);
}
function getShortQty() {
return adaptiveQuantity(QUANTITY[Math.min(SHORT_STEP, QUANTITY.length - 1)], exchange.GetTicker().Last);
}
// ===== 多头 =====
function firstManyBuy() {
if (MANY_BUYING) return;
const price = safePrice(exchange.GetTicker().Last);
const amount = safeAmount(getManyQty());
const orderId = placeOrder('buy', price, amount);
if (!orderId) return;
MANY_BUYING = true;
while (true) {
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
MANY_NEXT_BUY_PRICE = order.Price * ((100 - DOUBLE_THROW_RATIO) / 100);
MANY_BUYING = false;
MANY_EMPTY_STEP_TIME = null;
addPosition(MANY_ORDER_LIST, order.Price, order.Amount, LONG_SIDE);
break;
}
}
}
function manyBuy() {
if (MANY_BUYING) return;
const ticker = exchange.GetTicker();
if (!ticker || ticker.Last > MANY_NEXT_BUY_PRICE) return;
const price = safePrice(ticker.Last);
const amount = safeAmount(getManyQty());
const orderId = placeOrder('buy', price, amount);
if (!orderId) return;
MANY_BUYING = true;
MANY_BUY_TIME = Unix();
const expire = MANY_BUY_TIME + 60 * 30;
while (true) {
if (Unix() >= expire) { exchange.CancelOrder(orderId); MANY_BUYING = false; return; }
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
MANY_NEXT_BUY_PRICE = order.Price * ((100 - DOUBLE_THROW_RATIO) / 100);
MANY_BUYING = false;
MANY_EMPTY_STEP_TIME = null;
addPosition(MANY_ORDER_LIST, order.Price, order.Amount, LONG_SIDE);
break;
}
}
}
function manySell() {
const ticker = exchange.GetTicker();
if (!ticker) return;
for (let i = MANY_ORDER_LIST.length - 1; i >= 0; i--) {
const p = MANY_ORDER_LIST[i];
if (ticker.Last >= p.price * ((100 + PROFIT_RATIO) / 100)) {
const pos = exchange.GetPosition();
const longPos = pos.find(x => x.Type === 0 && x.Amount > 0);
if (!longPos) break;
const qty = safeAmount(Math.min(p.amount, longPos.Amount));
const orderId = placeOrder('closebuy', ticker.Last, qty);
if (!orderId) continue;
while (true) {
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
MANY_NEXT_BUY_PRICE = ticker.Last * ((100 - BUY_PRICE_RATIO) / 100);
removePosition(MANY_ORDER_LIST, i);
MANY_STEP = MANY_ORDER_LIST.length;
if (MANY_STEP === 0) {
MANY_EMPTY_STEP_TIME = Unix();
MANY_LAST_REANCHOR_TIME = Unix();
}
break;
}
}
}
}
}
// ===== 空头 =====
function firstShortBuy() {
if (SHORT_BUYING) return;
const price = safePrice(exchange.GetTicker().Last);
const amount = safeAmount(getShortQty());
const orderId = placeOrder('sell', price, amount);
if (!orderId) return;
SHORT_BUYING = true;
while (true) {
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
SHORT_NEXT_BUY_PRICE = order.Price * ((100 + DOUBLE_THROW_RATIO) / 100);
SHORT_BUYING = false;
SHORT_EMPTY_STEP_TIME = null;
addPosition(SHORT_ORDER_LIST, order.Price, order.Amount, SHORT_SIDE);
break;
}
}
}
function shortBuy() {
if (SHORT_BUYING) return;
const ticker = exchange.GetTicker();
if (!ticker || ticker.Last < SHORT_NEXT_BUY_PRICE) return;
const price = safePrice(ticker.Last);
const amount = safeAmount(getShortQty());
const orderId = placeOrder('sell', price, amount);
if (!orderId) return;
SHORT_BUYING = true;
SHORT_BUY_TIME = Unix();
const expire = SHORT_BUY_TIME + 60 * 30;
while (true) {
if (Unix() >= expire) { exchange.CancelOrder(orderId); SHORT_BUYING = false; return; }
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
SHORT_NEXT_BUY_PRICE = order.Price * ((100 + DOUBLE_THROW_RATIO) / 100);
SHORT_BUYING = false;
SHORT_EMPTY_STEP_TIME = null;
addPosition(SHORT_ORDER_LIST, order.Price, order.Amount, SHORT_SIDE);
break;
}
}
}
function shortSell() {
const ticker = exchange.GetTicker();
if (!ticker) return;
for (let i = SHORT_ORDER_LIST.length - 1; i >= 0; i--) {
const p = SHORT_ORDER_LIST[i];
if (ticker.Last <= p.price * ((100 - PROFIT_RATIO) / 100)) {
const pos = exchange.GetPosition();
const shortPos = pos.find(x => x.Type === 1 && x.Amount > 0);
if (!shortPos) break;
const qty = safeAmount(Math.min(p.amount, shortPos.Amount));
const orderId = placeOrder('closesell', ticker.Last, qty);
if (!orderId) continue;
while (true) {
const order = exchange.GetOrder(orderId);
if (!order) continue;
if (order.Status === 1 || order.Status === 2) {
SHORT_NEXT_BUY_PRICE = ticker.Last * ((100 + BUY_PRICE_RATIO) / 100);
removePosition(SHORT_ORDER_LIST, i);
SHORT_STEP = SHORT_ORDER_LIST.length;
if (SHORT_STEP === 0) {
SHORT_EMPTY_STEP_TIME = Unix();
SHORT_LAST_REANCHOR_TIME = Unix();
}
break;
}
}
}
}
}
// ===== 重锚 =====
function reAnchor() {
const now = Unix();
const ticker = exchange.GetTicker();
if (!ticker) return;
if (MANY_STEP === 0 && MANY_EMPTY_STEP_TIME) {
if (now >= MANY_EMPTY_STEP_TIME + REANCHOR_MINUTE * 60 ||
now >= MANY_LAST_REANCHOR_TIME + FORCE_REANCHOR_HOURS * 3600) {
MANY_NEXT_BUY_PRICE = ticker.Last * ((100 - DOUBLE_THROW_RATIO) / 100);
MANY_EMPTY_STEP_TIME = now;
MANY_LAST_REANCHOR_TIME = now;
}
}
if (SHORT_STEP === 0 && SHORT_EMPTY_STEP_TIME) {
if (now >= SHORT_EMPTY_STEP_TIME + REANCHOR_MINUTE * 60 ||
now >= SHORT_LAST_REANCHOR_TIME + FORCE_REANCHOR_HOURS * 3600) {
SHORT_NEXT_BUY_PRICE = ticker.Last * ((100 + DOUBLE_THROW_RATIO) / 100);
SHORT_EMPTY_STEP_TIME = now;
SHORT_LAST_REANCHOR_TIME = now;
}
}
}
// ===== 主循环 =====
function onTick() {
const ticker = exchange.GetTicker();
if (!ticker) return;
const markPrice = ticker.Last;
if (vol_history.length === 0) vol_history.push(markPrice);
const log_ret = Math.log(markPrice / (log_prices[log_prices.length - 1] || markPrice));
pushReturn(log_ret, min1_returns, 60);
pushReturn(log_ret, min5_returns, 300);
const jump1 = detectJump(log_ret, min1_returns);
const jump5 = detectJump(log_ret, min5_returns);
const account = exchange.GetAccount();
const accountNotional = account ? account.Balance : 2000;
const longES = computeES(MANY_ORDER_LIST, markPrice, accountNotional);
const shortES = computeES(SHORT_ORDER_LIST, markPrice, accountNotional);
let pauseLong = false, pauseShort = false;
if (longES > ES_THRESHOLD) pauseLong = true;
if (shortES > ES_THRESHOLD) pauseShort = true;
if (jump1 || jump5) {
if (log_ret < 0) pauseLong = true;
if (log_ret > 0) pauseShort = true;
}
comboClose(markPrice, MANY_ORDER_LIST, LONG_SIDE);
comboClose(markPrice, SHORT_ORDER_LIST, SHORT_SIDE);
trendBrake(markPrice);
if (trend_lock) return;
const band = dynamicBand();
PROFIT_RATIO = band.profit;
DOUBLE_THROW_RATIO = band.dthrow;
reAnchor();
if (MANY_ORDER_LIST.length === 0) firstManyBuy();
if (SHORT_ORDER_LIST.length === 0) firstShortBuy();
if (!pauseLong) {
manyBuy();
manySell();
}
if (!pauseShort) {
shortBuy();
shortSell();
}
}
function main() {
exchange.SetContractType("swap");
const initPrice = exchange.GetTicker().Last;
if (!initPrice || initPrice <= 0) throw new Error("Init price invalid");
MANY_NEXT_BUY_PRICE = safePrice(initPrice * ((100 - DOUBLE_THROW_RATIO) / 100));
SHORT_NEXT_BUY_PRICE = safePrice(initPrice * ((100 + DOUBLE_THROW_RATIO) / 100));
vol_history.push(initPrice);
MANY_LAST_REANCHOR_TIME = Unix();
SHORT_LAST_REANCHOR_TIME = Unix();
while (true) {
onTick();
Sleep(60000);
}
}