[原创] 股票多品种平衡策略设计教学

鉴于股票策略在FMZ.CN上相对较少,喜欢研究股票程序化、量化交易的同学们借鉴、参考的内容不多。为了更加方便程序化、量化交易入门的同学学习、研究。本篇我们一起来实现一个简单的股票多品种策略设计。OK!撸起袖子加油~宽客们。

策略思路

策略思路很简单,借鉴于FMZ.COM站的一个策略。同类型的策略在不可描述的区块市场上也有所作为。

策略逻辑简单说就是:
以账户初期资产为基数,根据策略参数设置一个百分比,计算出每只股票的持有价值。设置的股票池参数中每只股票按持有价值持仓。当价格上涨,使持仓的价值超过既定的持仓价值时,卖出一部分股票平衡到最初的持仓价值。当价格下跌,使持仓的价值低于既定的持仓价值时,买入一部分股票平衡到最初的持仓价值。

策略是不是非常简单。

实现策略

参数设计:

策略代码:

/*backtest
start: 2016-05-01 09:00:00
end: 2022-03-06 15:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_XTP","currency":"STOCK","balance":100000,"minFee":0}]
args: [["contractTypes","600519.SH,600690.SH,600006.SH,601328.SH,600887.SH,600121.SH,601633.SH"]]
*/

// 全局变量
var StatusMsg = ""

function newDate() {
    var timezone = 8                                //目标时区时间,东八区
    var offset_GMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟
    var nowDate = new Date().getTime()              // 本地时间距 1970 年 1 月 1 日午夜(GMT 时间)之间的毫秒数
    var targetDate = new Date(nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000)
    return targetDate
}

function GetPosition(e, contractTypeName) {
    var allAmount = 0
    var allProfit = 0
    var allFrozen = 0
    var posMargin = 0
    var price = 0
    var direction = null
    positions = _C(e.GetPosition)
    for (var i = 0; i < positions.length; i++) {
        if (positions[i].ContractType != contractTypeName) {
            continue
        }
        if (positions[i].Type == PD_LONG) {
            posMargin = positions[i].MarginLevel
            allAmount += positions[i].Amount
            allProfit += positions[i].Profit
            allFrozen += positions[i].FrozenAmount
            price = positions[i].Price
            direction = positions[i].Type
        }
    }
    if (allAmount === 0) {
        return null
    }
    return {
        MarginLevel: posMargin,
        FrozenAmount: allFrozen,
        Price: price,
        Amount: allAmount,
        Profit: allProfit,
        Type: direction,
        ContractType: contractTypeName,
        CanCoverAmount: allAmount - allFrozen
    }
}

function Buy(e, contractType, opAmount, insDetail) {
    var initPosition = GetPosition(e, contractType)
    var isFirst = true
    var initAmount = initPosition ? initPosition.Amount : 0
    var positionNow = initPosition
    if(opAmount % insDetail.LotSize != 0) {
        throw "每手数量不匹配"
    }
    while (true) {
        var needOpen = opAmount
        if (isFirst) {
            isFirst = false
        } else {
        	Sleep(Interval*20)
            positionNow = GetPosition(e, contractType)
            if (positionNow) {
                needOpen = opAmount - (positionNow.Amount - initAmount)
            }
            Log("positionNow:", positionNow, "needOpen:", needOpen)// 测试
        }
        if (needOpen < insDetail.LotSize || needOpen % insDetail.LotSize != 0) {
            break
        }
        var depth = _C(e.GetDepth)
        // 需要检测是否涨跌停
        var amount = needOpen
        e.SetDirection("buy")
        var orderId = e.Buy(depth.Asks[0].Price + (insDetail.PriceSpread * SlideTick), amount, contractType, 'Ask', depth.Asks[0])
        // CancelPendingOrders
        while (true) {
            Sleep(Interval*20)
            var orders = _C(e.GetOrders)
            if (orders.length === 0) {
                break
            }
            for (var j = 0; j < orders.length; j++) {
                e.CancelOrder(orders[j].Id)
                if (j < (orders.length - 1)) {
                    Sleep(Interval*20)
                }
            }
        }
    }
    var ret = null
    if (!positionNow) {
        return ret
    }
    ret = positionNow
    return ret
}

function Sell(e, contractType, lots, insDetail) {
    var initAmount = 0
    var firstLoop = true
    if(lots % insDetail.LotSize != 0) {
        throw "每手数量不匹配"
    }
    while (true) {
        var n = 0
        var total = 0
        var positions = _C(e.GetPosition)
        var nowAmount = 0
        for (var i = 0; i < positions.length; i++) {
            if (positions[i].ContractType != contractType) {
                continue
            }
            nowAmount += positions[i].Amount
        }
        if (firstLoop) {
            initAmount = nowAmount
            firstLoop = false
        }
        var amountChange = initAmount - nowAmount
        if (typeof(lots) == 'number' && amountChange >= lots) {
            break
        }

        for (var i = 0; i < positions.length; i++) {
            if (positions[i].ContractType != contractType) {
                continue
            }
            var amount = positions[i].Amount
            var depth
            var opAmount = 0
            var opPrice = 0
            if (positions[i].Type == PD_LONG) {
                depth = _C(e.GetDepth)
                // 需要检测是否涨跌停
                opAmount = amount
                opPrice = depth.Bids[0].Price - (insDetail.PriceSpread * SlideTick)
            }
            if (typeof(lots) === 'number') {
                opAmount = Math.min(opAmount, lots - (initAmount - nowAmount))
            }
            if (opAmount > 0) {
                if (positions[i].Type == PD_LONG) {
                    e.SetDirection("closebuy")
                    e.Sell(opPrice, opAmount, contractType, "平仓", 'Bid', depth.Bids[0])
                }
                n++
            }
            // break to check always
            if (typeof(lots) === 'number') {
                break
            }
        }
        if (n === 0) {
            break
        }
        while (true) {
            Sleep(Interval*20)
            var orders = _C(e.GetOrders)
            if (orders.length === 0) {
                break
            }
            for (var j = 0; j < orders.length; j++) {
                e.CancelOrder(orders[j].Id)
                if (j < (orders.length - 1)) {
                    Sleep(Interval*20)
                }
            }
        }
    }
}

/*
1、9:15-9:25为开盘集合竞价;
2、9:30-11:30,13:00-14:57为连续竞价阶段;
3、14:57-15:00为收盘集合竞价。
*/
function IsTrading() {
    var now = newDate()            // 使用 newDate() 代替 new Date() 因为服务器时区问题
    var day = now.getDay()
    var hour = now.getHours()
    var minute = now.getMinutes()
    StatusMsg = "非交易时段"

    if (day === 0 || day === 6) {
        return false
    }

    if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) {
    	// 9:30-11:30
        StatusMsg = "交易时段"
        return true 
    } else if (hour >= 13 && hour < 15) {
    	// 13:00-15:00
        StatusMsg = "交易时段"
        return true 
    }
    
    return false 
}

function main(){	
	var cts = contractTypes.split(",")
    var acc = _C(exchange.GetAccount)
    var value = _N(acc.Balance * ratio, 0)
    while (true) {
        var rowsData = []
        _.each(cts, function(ct) {
            if (!IsTrading()) {
                return
            }
            
            var info = exchange.SetContractType(ct)
            if (!info) {
                return 
            }

            var ticker = exchange.GetTicker()
            if (!ticker) {
                return 
            }           
            
            if (IsVirtual()) {
                ticker.Info = {}
                ticker.Info.LotSize = info.VolumeMultiple
                ticker.Info.PriceSpread = 0.01
            }
            
            var pos = GetPosition(exchange, ct)
            if (!pos) {
                // 没有持仓,按照价值建仓
                var amount = parseInt(value / ticker.Last / info.VolumeMultiple)
                if (amount < 1) {
                    return 
                }
                amount = amount * info.VolumeMultiple
                Buy(exchange, ct, amount, ticker.Info)                
            } else {
                // 计算价值偏离,平衡仓位
                var nowValue = ticker.Last * pos.Amount 
                var diffValue = nowValue - value
                if (diffValue > 0) {
                    // 持有价值增加超过阈值
                    var amount = parseInt(diffValue / ticker.Last / info.VolumeMultiple)
                    if (amount >= 1) {
                        // 平衡                        
                        amount = amount * info.VolumeMultiple
                        Sell(exchange, ct, amount, ticker.Info)
                        var newPos = GetPosition(exchange, ct)
                        Log("当前持仓价值:", pos.Amount + " * " + ticker.Last + " = " + nowValue, ",初期价值:", value, ",本次平衡价值:", amount * ticker.Last, ",平衡后的持仓价值:", newPos.Amount * ticker.Last)
                    }
                } else if (diffValue < 0) {
                    // 持有价值减少超过阈值
                    var amount = parseInt(-diffValue / ticker.Last / info.VolumeMultiple)
                    if (amount >= 1) {
                        // 平衡
                        amount = amount * info.VolumeMultiple
                        Buy(exchange, ct, amount, ticker.Info)
                        var newPos = GetPosition(exchange, ct)
                        Log("当前持仓价值:", pos.Amount + " * " + ticker.Last + " = " + nowValue, ",初期价值:", value, ",本次平衡价值:", amount * ticker.Last, ",平衡后的持仓价值:", newPos.Amount * ticker.Last)
                    }
                }
            }
                        
            rowsData.push([info.InstrumentID, info.InstrumentName, info.VolumeMultiple, ticker.Last, pos ? pos.Price : "--", pos ? pos.Amount : "--", value, pos ? ticker.Last * pos.Amount : "--"])
        })
        var tbl = {
            "type" : "table", 
            "title" : "行情、持仓数据", 
            "cols" : ["股票代码", "名称", "一手乘数", "当前价格", "持仓价格", "持仓数量", "初期价值", "当前价值"], 
            "rows" : rowsData
        }
        LogStatus("日期时间:", _D(), ", 状态信息:", StatusMsg, "\n`" + JSON.stringify(tbl) + "`")
        Sleep(1000)
    }
}

策略主要逻辑就在于对于持仓价值的计算。计算偏移出的价值是否足够至少一手的交易量。

            if (!pos) {
                // 没有持仓,按照价值建仓
                var amount = parseInt(value / ticker.Last / info.VolumeMultiple)
                if (amount < 1) {
                    return 
                }
                amount = amount * info.VolumeMultiple
                Buy(exchange, ct, amount, ticker.Info)                
            } else {
                // 计算价值偏离,平衡仓位
                var nowValue = ticker.Last * pos.Amount 
                var diffValue = nowValue - value
                if (diffValue > 0) {
                    // 持有价值增加超过阈值
                    var amount = parseInt(diffValue / ticker.Last / info.VolumeMultiple)
                    if (amount >= 1) {
                        // 平衡                        
                        amount = amount * info.VolumeMultiple
                        Sell(exchange, ct, amount, ticker.Info)
                        var newPos = GetPosition(exchange, ct)
                        Log("当前持仓价值:", pos.Amount + " * " + ticker.Last + " = " + nowValue, ",初期价值:", value, ",本次平衡价值:", amount * ticker.Last, ",平衡后的持仓价值:", newPos.Amount * ticker.Last)
                    }
                } else if (diffValue < 0) {
                    // 持有价值减少超过阈值
                    var amount = parseInt(-diffValue / ticker.Last / info.VolumeMultiple)
                    if (amount >= 1) {
                        // 平衡
                        amount = amount * info.VolumeMultiple
                        Buy(exchange, ct, amount, ticker.Info)
                        var newPos = GetPosition(exchange, ct)
                        Log("当前持仓价值:", pos.Amount + " * " + ticker.Last + " = " + nowValue, ",初期价值:", value, ",本次平衡价值:", amount * ticker.Last, ",平衡后的持仓价值:", newPos.Amount * ticker.Last)
                    }
                }
            }

策略回测

平衡策略用于操作多只股票组成的股票池,可以分散投资。回测中每只股票投入资金只有5000元,并且根据价格变动不断调整持仓。由于策略开始需要持有底仓,等于开始做多。所以最好还是选择一些潜力较好的股票。

策略地址:https://www.fmz.cn/strategy/349108

当然,策略回测仅供于参考。策略设计仅仅为了学习、研究,实盘还需进一步的优化、升级。

对于0基础的小伙伴来说以上的策略代码可能有些难懂。所以,对于不会编程、交易的小伙伴们推荐一本非常实用的书籍,无缝贴合http://FMZ.CN量化交易平台的量化交易实战,会非常方便入门量化交易领域。

 

 

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线