My语言/PINE语言实盘跟单策略


创建日期: 2025-05-21 11:03:25 最后修改: 2025-05-23 15:13:18
复制: 0 点击次数: 52
avatar of 发明者量化-小小梦 发明者量化-小小梦
4
关注
1143
关注者
策略源码
/*backtest
start: 2024-05-21 00:00:00
end: 2025-05-20 00:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_Binance","currency":"ETH_USDT"},{"eid":"Futures_Binance","currency":"ETH_USDT"},{"eid":"Futures_Binance","currency":"ETH_USDT","balance":10000}]
args: [["isBacktest",true]]
*/

class Leader {
    constructor(leaderCfg = { type: "exchange", apiClient: exchanges[0] }) {
        // 带单账户配置
        this.leaderCfg = leaderCfg
        // 缓存上次持仓信息,用于对比
        this.lastPos = null
        // 记录当前持仓信息
        this.currentPos = null
        // 记录订阅者
        this.subscribers = []

        // 根据 leaderCfg 配置,确定使用哪种监控方案:1、直接监控带单账户。2、通过FMZ 扩展API监控带单策略实盘的数据。3、消息推送机制跟单。默认使用方案1。

        // 初始化
        let ex = this.leaderCfg.apiClient
        let currency = ex.GetCurrency()
        let arrCurrency = currency.split("_")
        if (arrCurrency.length !== 2) {
            throw new Error("带单账户配置错误,必须是两种币对")
        }
        this.baseCurrency = arrCurrency[0]
        this.quoteCurrency = arrCurrency[1]
        // 获取初始化时,带单账户的总权益
        this.initEquity = _C(ex.GetAccount).Equity
        this.currentPos = _C(ex.GetPositions)
    }

    // 监控leader的逻辑
    poll() {
        // 获取交易所对象
        let ex = this.leaderCfg.apiClient

        // 获取leader的持仓、账户资产数据
        let pos = ex.GetPositions()
        if (!pos) {
            return
        }
        this.currentPos = pos

        // 调用判断方法,判断持仓变化
        let { hasChanged, diff } = this._hasChanged(pos)
        if (hasChanged) {
            Log("Leader持仓变化,当前持仓:", pos)
            Log("持仓变动:", diff)
            // 通知订阅者
            this.subscribers.forEach(subscriber => {
                subscriber.applyPosChanges(diff)
            })
        }

        // 同步持仓
        this.subscribers.forEach(subscriber => {
            subscriber.syncPositions(pos)
        })
    }

    // 判断持仓是否变化
    _hasChanged(pos) {
        if (this.lastPos) {
            // 用于存储持仓差异的结果
            let diff = {
                added: [],    // 新增的持仓
                removed: [],  // 移除的持仓
                updated: []   // 更新的持仓(数量或价格变化)
            }

            // 将上次持仓和当前持仓转换为 Map,键为 `symbol + direction`,值为持仓对象
            let lastPosMap = new Map(this.lastPos.map(p => [`${p.Symbol}|${p.Type}`, p]))
            let currentPosMap = new Map(pos.map(p => [`${p.Symbol}|${p.Type}`, p]))

            // 遍历当前持仓,找出新增和更新的持仓
            for (let [key, current] of currentPosMap) {
                if (!lastPosMap.has(key)) {
                    // 如果上次持仓中没有该 key,则为新增持仓
                    diff.added.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount })
                } else {
                    // 如果存在,则检查数量或价格是否变化
                    let last = lastPosMap.get(key)
                    if (current.Amount !== last.Amount) {
                        diff.updated.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount - last.Amount })
                    }
                    // 从 lastPosMap 中移除,剩下的就是被移除的持仓
                    lastPosMap.delete(key)
                }
            }

            // 剩余在 lastPosMap 中的 key 是被移除的持仓
            for (let [key, last] of lastPosMap) {
                diff.removed.push({ symbol: last.Symbol, type: last.Type, deltaAmount: -last.Amount })
            }

            // 判断是否有变化
            let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0

            // 如果有变化,更新 lastPos
            if (hasChanged) {
                this.lastPos = pos
            }

            return { hasChanged: hasChanged, diff: diff }
        } else {
            // 如果没有上次持仓记录,更新记录,不做持仓同步
            this.lastPos = pos
            return { hasChanged: false, diff: { added: [], removed: [], updated: [] } }

            /* 另一种方案:同步仓位
            if (pos.length > 0) {
                let diff = {
                    added: pos.map(p => ({symbol: p.Symbol, type: p.Type, deltaAmount: p.Amount})),
                    removed: [],
                    updated: []
                }
                return {hasChanged: true, diff: diff}
            } else {
                return {hasChanged: false, diff: {added: [], removed: [], updated: []}}
            }
            */
        }
    }

    // 订阅者注册
    subscribe(subscriber) {
        if (this.subscribers.indexOf(subscriber) === -1) {
            if (this.quoteCurrency !== subscriber.quoteCurrency) {
                throw new Error("订阅者币对不匹配,当前leader币对:" + this.quoteCurrency + ",订阅者币对:" + subscriber.quoteCurrency)
            }
            if (subscriber.followStrategy.followMode === "equity_ratio") {
                // 设置跟单比例
                let ex = this.leaderCfg.apiClient
                let equity = _C(ex.GetAccount).Equity
                subscriber.setEquityRatio(equity)
            }
            this.subscribers.push(subscriber)
            Log("订阅者注册成功,订阅配置:", subscriber.getApiClientInfo())
        }
    }

    // 取消订阅
    unsubscribe(subscriber) {
        const index = this.subscribers.indexOf(subscriber)
        if (index !== -1) {
            this.subscribers.splice(index, 1)
            Log("订阅者取消注册成功,订阅配置:", subscriber.getApiClientInfo())
        } else {
            Log("订阅者取消注册失败,订阅配置:", subscriber.getApiClientInfo())
        }
    }

    // 获取UI信息
    fetchLeaderUI() {
        // 带单者信息
        let tblLeaderInfo = { "type": "table", "title": "Leader Info", "cols": ["带单方案", "计价币种", "跟单者数量", "初始总权益"], "rows": [] }
        tblLeaderInfo.rows.push([this.leaderCfg.type, this.quoteCurrency, this.subscribers.length, this.initEquity])

        // 构造带单者的显示信息:持仓信息
        let tblLeaderPos = { "type": "table", "title": "Leader pos", "cols": ["交易品种", "方向", "数量", "价格"], "rows": [] }
        this.currentPos.forEach(pos => {
            let row = [pos.Symbol, pos.Type == PD_LONG ? "多" : "空", pos.Amount, pos.Price]
            tblLeaderPos.rows.push(row)
        })

        // 构造订阅者的显示信息
        let strFollowerMsg = ""
        this.subscribers.forEach(subscriber => {
            let arrTbl = subscriber.fetchFollowerUI()
            strFollowerMsg += "`" + JSON.stringify(arrTbl) + "`\n"
        })

        return "`" + JSON.stringify([tblLeaderInfo, tblLeaderPos]) + "`\n" + strFollowerMsg
    }

    // 扩展暂停跟单、移除订阅等功能
}

class Subscriber {
    constructor(subscriberCfg, followStrategy = { followMode: "position_ratio", ratio: 1, maxReTries: 3 }) {
        this.subscriberCfg = subscriberCfg
        this.followStrategy = followStrategy

        // 初始化
        let ex = this.subscriberCfg.apiClient
        let currency = ex.GetCurrency()
        let arrCurrency = currency.split("_")
        if (arrCurrency.length !== 2) {
            throw new Error("订阅者配置错误,必须是两种币对")
        }
        this.baseCurrency = arrCurrency[0]
        this.quoteCurrency = arrCurrency[1]

        // 初始获取持仓数据
        this.currentPos = _C(ex.GetPositions)
        this.markets = _C(ex.GetMarkets)
    }

    setEquityRatio(leaderEquity) {
        // {followMode: "equity_ratio"} 自动根据账户权益比例跟单
        if (this.followStrategy.followMode === "equity_ratio") {
            let ex = this.subscriberCfg.apiClient
            let equity = _C(ex.GetAccount).Equity
            let ratio = equity / leaderEquity
            this.followStrategy.ratio = ratio
            Log("带单者权益:", leaderEquity, "订阅者权益:", equity)
            Log("自动设置,订阅者权益比例:", ratio)
        }
    }

    getSymbolPrecision(symbol) {
        // 尝试从缓存中获取
        let market = this.markets[symbol]

        // 如果没有命中缓存,则刷新市场信息并再尝试一次
        if (!market) {
            let ex = this.subscriberCfg.apiClient
            this.markets = _C(ex.GetMarkets)
            market = this.markets[symbol]
        }

        // 返回精度或 null
        return market ? {pricePrecision: market.PricePrecision, amountPrecision: market.AmountPrecision, minQty: market.MinQty} : null
    }

    // 获取订阅者绑定的API客户端信息
    getApiClientInfo() {
        let ex = this.subscriberCfg.apiClient
        let idx = this.subscriberCfg.clientIdx
        if (ex) {
            return { exName: ex.GetName(), exLabel: ex.GetLabel(), exIdx: idx, followStrategy: this.followStrategy }
        } else {
            throw new Error("订阅者没有绑定API客户端")
        }
    }

    // 根据持仓类型、仓位变化,返回交易方向参数
    getTradeSide(type, deltaAmount) {
        if (type == PD_LONG && deltaAmount > 0) {
            return "buy"
        } else if (type == PD_LONG && deltaAmount < 0) {
            return "closebuy"
        } else if (type == PD_SHORT && deltaAmount > 0) {
            return "sell"
        } else if (type == PD_SHORT && deltaAmount < 0) {
            return "closesell"
        }

        return null
    }

    getSymbolPosAmount(symbol, type) {
        let ex = this.subscriberCfg.apiClient
        if (ex) {
            let pos = _C(ex.GetPositions, symbol)
            if (pos.length > 0) {
                // 遍历持仓,查找对应的symbol和type
                for (let i = 0; i < pos.length; i++) {
                    if (pos[i].Symbol === symbol && pos[i].Type === type) {
                        return pos[i].Amount
                    }
                }
            }
            return 0
        } else {
            throw new Error("订阅者没有绑定API客户端")
        }
    }

    // 下单重试
    tryCreateOrder(ex, symbol, side, price, amount, label, maxReTries) {
        for (let i = 0; i < Math.max(maxReTries, 1); i++) {
            let orderId = ex.CreateOrder(symbol, side, price, amount, label)
            if (orderId) {
                return orderId
            }
            Sleep(1000)
        }
        return null
    }

    // 同步持仓变化
    applyPosChanges(diff) {
        let ex = this.subscriberCfg.apiClient
        if (ex) {
            ["added", "removed", "updated"].forEach(key => {
                diff[key].forEach(item => {
                    let side = this.getTradeSide(item.type, item.deltaAmount)
                    if (side) {
                        // 获取精度等信息
                        let symbolInfo = this.getSymbolPrecision(item.symbol)
                        if (!symbolInfo) {
                            Log("获取品种信息失败,symbol:", item.symbol)
                            return 
                        }
                        ex.SetPrecision(symbolInfo.pricePrecision, symbolInfo.amountPrecision)

                        // 计算跟单比例
                        let ratio = this.followStrategy.ratio
                        let tradeAmount = Math.abs(item.deltaAmount) * ratio
                        if (side == "closebuy" || side == "closesell") {
                            // 获取持仓数量检查
                            let posAmount = this.getSymbolPosAmount(item.symbol, item.type)
                            tradeAmount = Math.min(posAmount, tradeAmount)
                        }
                        // 检查订单下单量是否符合品种要求
                        if (_N(tradeAmount, symbolInfo.amountPrecision) < symbolInfo.minQty) {
                            Log("下单量小于该品种在交易所要求的最小量,symbol:", item.symbol, ", tradeAmount:", tradeAmount, ", symbolInfo.minQty:", symbolInfo.minQty)
                            return 
                        }
                        // 订单Id
                        let orderId = this.tryCreateOrder(ex, item.symbol, side, -1, tradeAmount, ex.GetLabel(), this.followStrategy.maxReTries)
                        // 检测订单Id
                        if (orderId) {
                            Log("订阅者下单成功,订单Id:", orderId, ",下单方向:", side, ",下单数量:", Math.abs(item.deltaAmount), ",跟单比例(倍):", ratio)
                        } else {
                            Log("订阅者下单失败,订单Id:", orderId, ",下单方向:", side, ",下单数量:", Math.abs(item.deltaAmount), ",跟单比例(倍):", ratio)
                        }
                    }
                })
            })

            // 更新当前持仓
            this.currentPos = _C(ex.GetPositions)
        } else {
            throw new Error("订阅者没有绑定API客户端")
        }
    }

    // 同步持仓
    syncPositions(leaderPos) {
        let ex = this.subscriberCfg.apiClient
        this.currentPos = _C(ex.GetPositions)

        // 用于存储持仓差异的结果
        let diff = {
            added: [],    // 新增的持仓
            removed: [],  // 移除的持仓
            updated: []   // 更新的持仓(数量或价格变化)
        }
        let leaderPosMap = new Map(leaderPos.map(p => [`${p.Symbol}|${p.Type}`, p]))
        let currentPosMap = new Map(this.currentPos.map(p => [`${p.Symbol}|${p.Type}`, p]))
        // 遍历当前持仓,找出新增和更新的持仓
        for (let [key, leader] of leaderPosMap) {
            if (!currentPosMap.has(key)) {
                diff.added.push({ symbol: leader.Symbol, type: leader.Type, deltaAmount: leader.Amount })
            } else {
                let current = currentPosMap.get(key)
                if (leader.Amount !== current.Amount) {
                    diff.updated.push({ symbol: leader.Symbol, type: leader.Type, deltaAmount: leader.Amount - current.Amount * this.followStrategy.ratio })
                }
                currentPosMap.delete(key)
            }
        }
        for (let [key, current] of currentPosMap) {
            diff.removed.push({ symbol: current.Symbol, type: current.Type, deltaAmount: -current.Amount * this.followStrategy.ratio })
        }
        // 判断是否有变化
        let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0
        if (hasChanged) {
            // 同步
            this.applyPosChanges(diff)
        }
    }

    // 获取订阅者UI信息
    fetchFollowerUI() {
        // 订阅者信息
        let ex = this.subscriberCfg.apiClient
        let equity = _C(ex.GetAccount).Equity
        let exLabel = ex.GetLabel()
        let tblFollowerInfo = { "type": "table", "title": "Follower Info", "cols": ["交易所对象索引", "交易所对象标签", "计价币种", "跟单模式", "跟单比例(倍)", "最大重试次数", "总权益"], "rows": [] }
        tblFollowerInfo.rows.push([this.subscriberCfg.clientIdx, exLabel, this.quoteCurrency, this.followStrategy.followMode, this.followStrategy.ratio, this.followStrategy.maxReTries, equity])

        // 订阅者持仓信息
        let tblFollowerPos = { "type": "table", "title": "Follower pos", "cols": ["交易品种", "方向", "数量", "价格"], "rows": [] }
        let pos = this.currentPos
        pos.forEach(p => {
            let row = [p.Symbol, p.Type == PD_LONG ? "多" : "空", p.Amount, p.Price]
            tblFollowerPos.rows.push(row)
        })

        return [tblFollowerInfo, tblFollowerPos]
    }
}

// 测试函数,模拟随机开仓,模拟leader仓位变动
function randomTrade(symbol, amount) {
    let randomNum = Math.random()
    if (randomNum < 0.0001) {
        Log("模拟带单交易", "#FF0000")
        let ex = exchanges[0]
        let pos = _C(ex.GetPositions)

        if (pos.length > 0) {
            // 随机平仓
            let randomPos = pos[Math.floor(Math.random() * pos.length)]
            let tradeAmount = Math.random() > 0.7 ? Math.abs(randomPos.Amount * 0.5) : Math.abs(randomPos.Amount)
            ex.CreateOrder(randomPos.Symbol, randomPos.Type === PD_LONG ? "closebuy" : "closesell", -1, tradeAmount, ex.GetLabel())
        } else {
            let tradeAmount = Math.random() * amount
            let side = Math.random() > 0.5 ? "buy" : "sell"
            if (side === "buy") {
                ex.CreateOrder(symbol, side, -1, tradeAmount, ex.GetLabel())
            } else {
                ex.CreateOrder(symbol, side, -1, tradeAmount, ex.GetLabel())
            }
        }
    }
}

// 策略主循环
function main() {
    let leader = new Leader()

    let followStrategyArr = JSON.parse(strFollowStrategyArr)
    if (followStrategyArr.length > 0 && followStrategyArr.length !== exchanges.length - 1) {
        throw new Error("跟单策略配置错误,跟单策略数量和交易所数量不匹配")
    }

    for (let i = 1; i < exchanges.length; i++) {
        let subscriber = null
        if (followStrategyArr.length == 0) {
            subscriber = new Subscriber({ apiClient: exchanges[i], clientIdx: i })
        } else {
            let followStrategy = followStrategyArr[i - 1]
            subscriber = new Subscriber({ apiClient: exchanges[i], clientIdx: i }, followStrategy)
        }
        leader.subscribe(subscriber)
    }

    // 启动监控
    while (true) {
        leader.poll()
        Sleep(1000 * pollInterval)

        // 模拟随机交易
        if (IsVirtual() && isBacktest) {
            // 为了能让GetMarkets函数获取到数据
            for (let i = 0 ; i < exchanges.length ; i++) {
                let ex = exchanges[i]
                ex.GetTicker("BTC_USDT.swap")
                ex.GetTicker("ETH_USDT.swap")
                ex.GetTicker("SOL_USDT.swap")
            }
            // 随机带单者开仓
            randomTrade("BTC_USDT.swap", 0.001)
            randomTrade("ETH_USDT.swap", 0.02)
            randomTrade("SOL_USDT.swap", 0.1)
        }

        LogStatus(_D(), "\n", leader.fetchLeaderUI())
    }
}