基于波动率与趋势检测的联合减仓双向网格合约


创建日期: 2025-08-30 22:51:18 最后修改: 2025-08-31 22:21:43
复制: 0 点击次数: 398
avatar of 智明 智明
0
关注
4
关注者

策略名称:基于波动率与趋势检测的联合网格交易策略

策略思路

1、基本源码是网友的策略:https://www.fmz.com/strategy/293325; 2、我在这个的基础上增加了联合减仓、风控等多项方案; 3、还可以增加冷静期、趋势跟踪、择时开单、防瀑布等多个方案(未更新);

策略概述

该策略是一种复杂的量化交易系统,旨在通过网格交易的方式在加密货币市场(如BTC/USDT)进行多空双向操作,同时结合多种统计与金融工程方法,对市场波动性、趋势特征进行动态分析,从而实现风险控制与收益优化。策略适用于1分钟频率的期货交易,并严格控制每笔交易的最小仓位单位。

核心逻辑与功能模块

1. 自适应波动率调整

  • 目标:根据市场波动性动态调整每笔订单的交易数量,确保在高波动性环境下降低仓位风险,在低波动性环境下提高仓位利用率。
  • 机制
    • 使用过去30个价格点的对数收益率构建波动率历史数据。
    • 通过标准差计算实时波动率,并与目标波动率(0.015)进行对比。
    • 根据波动率比例动态调整基础仓位,最多放大2倍。
  • 代码实现:通过updateVoladaptiveQuantity函数实现波动率计算与仓位调整。

2. 趋势检测(Hurst指数与CUSUM方法)

  • 目标:识别市场是否处于趋势状态,避免在强趋势环境下频繁开仓。
  • 机制
    • 使用Hurst指数分析价格序列的长期记忆性,判断市场处于趋势(Hurst > 0.55)或均值回归(Hurst < 0.45)状态。
    • 通过CUSUM算法实时监控对数收益率的累积偏差,识别趋势的启动与结束。
  • 代码实现:通过hurstcusumCheck函数实现趋势判断逻辑。

3. 联合网格交易与动态止损止盈

  • 目标:在市场波动中通过网格化订单捕捉利润,同时确保严格的风险控制。
  • 机制
    • 使用非对称网格,多头与空头订单分别管理。
    • 基于GARCH模型动态计算止损和止盈价位,确保价格波动覆盖预期波动范围。
    • 引入联合减仓机制,批量关闭亏损订单以控制风险。
  • 代码实现:通过comboClose函数实现联合减仓逻辑,通过dynamicBand函数动态调整止损止盈价位。

4. 跳跃检测与风险控制

  • 目标:识别市场中的异常价格波动(跳跃),避免极端行情下的交易风险。
  • 机制
    • 计算不同时间窗口(1分钟、5分钟)的收益率波动率。
    • 使用Z值判断当前收益率是否超出正常波动范围(阈值为2.326)。
  • 代码实现:通过detectJump函数实现跳跃检测逻辑。

5. 期望损失(ES)风险管理

  • 目标:控制投资组合的尾部风险,避免极端损失。
  • 机制
    • 计算历史持仓的亏损排序,提取条件期望损失(Expected Shortfall)。
    • 当ES超过预设阈值(0.05)时暂停对应方向交易。
  • 代码实现:通过computeES函数实现尾部风险计算。

6. 重锚机制

  • 目标:在仓位完全清空后,重新设置初始交易价格,确保策略持续适应最新市场环境。
  • 机制
    • 当某方向(多头或空头)订单全部关闭时,根据当前市场价格重新计算初始挂单价格。
    • 设置最小重锚时间和强制重锚时间,避免长期停滞。
  • 代码实现:通过reAnchor函数实现重锚逻辑。

策略优势

  1. 多维度风险控制:结合波动率调整、趋势检测、止损止盈、跳跃检测与ES管理,形成全方位的风险防控体系。
  2. 动态适应性:通过GARCH模型和Hurst指数,策略能够动态适应市场波动与趋势变化。
  3. 严格的仓位管理:联合减仓与网格交易相结合,确保在复杂市场环境中维持合理的仓位结构。
  4. 透明的决策逻辑:所有交易决策均基于明确的数学模型与统计指标,便于回测与优化。

适用场景与局限性

  • 适用场景:该策略适用于具有明显波动特征的加密货币市场,尤其适合在震荡行情中捕捉网格利润。
  • 局限性
    • 对市场流动性有一定要求,极端流动性枯竭情况下可能无法成交。
    • 参数(如波动率目标、利润比例等)需根据具体资产和市场环境调整。
    • 依赖历史数据统计特性,无法完全预测黑天鹅事件。

总结

该策略是一种高度工程化的量化交易系统,融合了现代金融工程中的多种方法论。它通过动态调整仓位、严格控制风险和利用市场波动特性,在加密货币市场中实现网格化利润捕捉。适合对量化交易与风险控制有一定了解的投资者作为参考。

策略源码

/*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);
    }
}
全部留言
avatar of 梦才
梦才
这个用于实盘的表现怎么样?
2025-09-06 23:29:47
avatar of ianzeng123
ianzeng123
点赞,点赞,交叉点赞!
2025-09-05 11:57:15