资源加载中... loading...

商品期货量化交易实践系列课程

Author: 雨幕(youquant), Created: 2023-09-12 09:54:22, Updated: 2023-12-18 16:43:42

提供了有关特定交易品种的当前买卖订单的详细情况,可以展示出不同价格水平上的委托量。通过观察市场深度数据,我们可以了解到当前市场上的买卖力量分布情况,以及在不同价格水平上的供需情况。这对于市场分析和制定交易策略非常重要,因为它可以帮助交易者判断支撑位和阻力位的价格水平,并预测市场的可能走势。但是市场深度数据是市场中尚未成交的订单流数据,这些数据就是各交易软件中常见的五档行情(如上图所示)。做过交易的都知道,这些数据通常变化无常,有时候突然来一个大单,又突然凭空消失,存在很强的欺骗和诱导作用。

image

成交量分布(VP):成交量分布(Volume Profile,简称VP)是一种用于衡量市场交易活跃度和价格区间的市场分析工具。它通过统计和显示特定时间段内的各个价格水平上的成交量信息。成交量分布通常以柱状图的形式展示,横轴表示价格区间,纵轴表示成交量。每个价格区间上的柱子高度表示该价格区间上的成交量。通过这种方式,交易者可以清晰地看到市场上不同价格水平的成交活动情况。根据成交量分布图,我们可以获得支撑位和阻力位预测区间;其次可以了解交易活跃度和流动性情况。但是呢,成交量分布具有一定的滞后性,成交量分布图是基于历史数据生成的,只能反映过去一段时间的市场情况。随着时间的推移,市场情况可能发生变化,导致成交量分布图的有效性降低。

image

足迹图(Footprint Chart):足迹图(Footprint Chart)是一种用于显示市场交易活动的特殊图表类型。它提供了更为详细和精确的成交价与成交量之间的关系,帮助交易者更好地理解市场的买卖压力以及价格变动。在足迹图中,每个价格水平都有一个指示器来表示该价格上的成交量。通常,买入成交量位于价格上方,卖出成交量位于价格下方。这种显示方式可以让我们看到市场上的买盘和卖盘活动,并评估哪一方力量更强。

image

足迹图可以提供以下关键信息:

成交量分布:足迹图显示了不同价格水平上的成交量分布情况。通过观察不同价格区间上的成交量,交易者可以了解到相应价格水平上的买入和卖出活动强度。

买卖压力:足迹图可帮助交易者判断买卖压力的分布情况。如果在特定价格水平上的买入成交量较高,可能表示买盘压力较大,可能导致价格上涨;而若卖出成交量较高,则可能意味着卖盘压力较大,可能导致价格下跌。

交易情绪:足迹图可以提供一定的交易情绪指示。例如,如果看到大量买盘活动并且价格上涨,可能表示市场参与者对价格有较强的偏好和买入需求。相反,如果出现大量卖盘活动且价格下跌,可能表示市场参与者更倾向于卖出,并且情绪较为悲观。

足迹图也是有一些缺点的,例如足迹图展示了每个价格级别上的买卖交易量,其中可能包含大量的小额交易,这些交易可能是市场噪音或者无意义的交易。这可能导致交易者对信息的过度解读或者误判市场趋势。

以上就是orderflow四种数据的介绍。其实在市场中,充斥着各种各样的信息,好的坏的、真的假的,这些信息就像荆棘一样错纵交织,导致这些消息很难被理解,很难用正确的逻辑推导。作为散户,资金和消息的弱势群体,在很多情况下只能被动的接受主力的安排,所以我们需要使用技术的工具抽取出价格波动的真正原因。

作为散户的我们使用肉眼观察Orderflow数据,获得的信息在一定程度上是片面和不完整的,因此做出的交易决策可能缺乏完整的依据。幸好,有了量化分析的帮助,我们可以从宏观视角去捕捉交易的每一刻变化,进而描绘出整体的趋势脉络。因此,本节课程,我们将要学习在优宽量化交易平台,尝试探索实现orderflow数据构建和实施交易策略。

构建成交量分布图

本节课呢,让我们来构建这个成交量分布图。在JavaScript语言中,并没有内置函数可以帮助我们快速获取结果。但是,我们可以利用目前所学的知识,手动编写代码来实现这个功能。

var chart = {
    __isStock: false,
    title: {
        text: '成交量分布图',
    },
    xAxis: {
      title: {
        text: 'Price'
      }
    },
    yAxis: {
      title: {
        text: 'Volume'
      }
    },
    series: [{
        type: 'column',
        data: []                
    }]
}

function main() {
    var isFirst = true
    var priceCount = {}
    var preVolume = 0
    while (true) {
        if(exchange.IO('status')){
            LogStatus('已连接CTP', _D())
            exchange.SetContractType('SA888')
            var ticker = exchange.GetTicker()
            var price = ticker.Last
            var volume = ticker.Volume

            if (volume < preVolume) {
                Log('更新时间到')
                priceCount = {}
                isFirst = true
                preVolume = 0
            }
            
            if(isFirst){
                preVolume = volume
                isFirst = false
            }else{
                if (!priceCount.hasOwnProperty(price)) {
                    priceCount[price] = volume - preVolume
                } else {
                    priceCount[price] += volume - preVolume
                }
                preVolume = volume
            }

            var pricecountlist = []

            for (var key in priceCount) {
                pricecountlist.push([parseInt(key), priceCount[key]])
            }

            chart.series[0].data = pricecountlist
            Chart(chart)
            
            Sleep(1000) 
        }else{
            LogStatus('未连接CTP', _D())
            Sleep(1000) 
        }
    }
}

来到策略编辑页面。第一步,我们来构建chart对象,这里将x轴和y轴分别命名为Price和Volume,series中将图表类型定义为column柱状图,data定义为空值,我们将在主函数进行填充。

接着我们来定义主函数,还是我们的固定框架,大家在写策略的时候,可以先把这个框架列好,然后进行填充。这里设置的合约是螺纹钢的主力合约。我们前面的准备工作已经完成了,现在我们来想下,我们应该获取什么数据,向图表里进行填充。K线数据吗,K线数据反映的是一段时间内价格的汇总变化,所以无法反应每一个价格具体的成交量。所以这里选择ticker数据,ticker数据可以反应价格的具体变化以及相对应的成交量。好了,目标数据我们选好了。但是直接拿ticker数据向图表里填充显然是不合适的。因为Ticker数据中的成交量是今日开盘累计的成交量,所以对于某一价格的成交量对于需要使用本时刻的成交量减去上一时刻的成交量;其次我们想要呈现的成交量分布是某一个具体开盘日的成交量数据,所以一天过后,我们需要及时的对储存的数据进行清空。

带着这两个问题,继续我们的代码编写,在while循环体外,首先设置三个变量,isFirst,是否第一个获取ticker数据;priceCount,用来储存价格成交量键值对;preVolume,先前ticker的成交量;在while循环中,获取当前ticker数据,然后获取最新成交价作为price,成交量作为volume。接着就要进行数据的填充了,如果是第一次获取ticker数据,先前的preVolume是没有的。所以将preVolume定义为第一个ticker的成交量,然后将isFirst设置为false。然后具体的价格和对应的成交量可以正常更新了。这里的priceCount我们用来储存价格和数量。如果priceCount没有某一price键,我们首先priceCouut添加该键,并将该键的值定义为本时刻的volume减去前一时刻的preVolume。如果存在该键的话,进行成交量的累加。一个ticker数据更新完成后,将preVolume定义为最新的volume。

注意:因为策略开盘的时候,ticker返回的成交量都是巨大的,不能真实反应某一价格的成交量,所以我们决定舍弃第一个ticker的数据。

成交量数据处理完毕了,接下来我们来看怎么进行数据的重置。volume是一个累加的数值,所以最新时刻的volume一定是大于等于上一时刻的,当最新时刻volume小于上一个时刻的时候,证明volume更新了,这里我们提升“更新时间到”,然后将priceCount,isFirst和preVolume进行重置。

好了,目前我们两个问题都解决了。接下来,我们就要将获取到的数据进行一下格式的整理,因为column需要获取的格式是数组的形式,每个数组的元素包含价格和对应的成交量,我们创建pricecountlist空的数组,然后将priceCount里的键值对进行添加,这里的键是字符型的,要进行一下转换。

最后将chart.series[0].data定义为pricecountlist,使用Chart进行画图。

我们来看下回测的结果,定义时间横跨两个交易日,这里需要使用的是实盘级tick。点击回测。螺纹钢的开盘时间是晚上九点,但是第一个ticker数据返回是8点59分,日志信息显示晚上8点59分,准时进行了数据的更新。这里的成交量分布图看起来是一个完美的正态分布,对应K线图,确实符合螺纹钢最近的震荡行情。所以,我们利用我们的一些统计学知识,在前提判断行情为震荡行情时,使用成交量最高的价格作为均线,然后使用95%的置信区间作为正常的波动范围,如果价格下穿或者上穿这个范围,我们进行相应的开多或者开空的交易操作。当然,并不是每日的交易日都是这样符合正态分布的,有时候会呈现偏态或者多峰分布,我们需要具体问题具体分析。

对统计学感兴趣的小伙伴,一看到这个分布一定会有很多交易策略的想法迸发出来,大家可以动手尝试一下,当然也可以留言评论区,我们呢,也会热心的帮你实现,我们一起来实现共同进步!

视频参考链接: 《CTA策略之orderflow订单流策略(1)》

12.商品期货Orderflow订单流策略(下)

上节课我们学习了Orderflow订单流之成交量分布的画图方法,本节课我们来继续学习足迹图的画法和相关的交易策略。

传统的K线有开盘价、最高价、最低价、收盘价等四个价格,然而K线仅仅代表这个时间段内的价格变化情况,比如小时线代表了一个小时内的价格变化情况。而足迹图则是根据Tick数据,提供了K线时间段内发生的具体细节,包括K线每个价格的多头和空头成交量,可以很清晰的看见在这一根k线柱中,具体价格多空成交的订单。

例如我们来看这个足迹图,每一根K线都有一个独立的Delta结构数据,在Delta结构数据方框中,最上方是这根K线总的成交量,最下方是这根K线所有多头成交量和空头成交量的差,中间则是这根K线每个价格多头成交量和空头成交量数据。通过将K线拆分成更详细的可视化数据,从而帮助我们理解价格变动的机制。

image

构建足迹图

今天我们首先利用代码实现一个足迹图(Footprint Chart)K线图表。

首先提醒一下大家哈,这部分的画图代码确实比较复杂,尤其是图表对象的构建,不过大家也不用太纠结。如果大家对图表对象中哪些属性不太清楚,可以询问chatGPT直接获取答案。当我们需要足迹图的时候,我们可以直接拿过来使用。不过这里重要的是,我们要理解足迹图中,对于ticker数据的处理,其中关键的是买方数量和卖方数量的获取逻辑,这样有助于我们更深入地理解价格和买卖数量的变动关系。

因为足迹图K线图表画图是比较复杂的,这个策略代码分为两个部分。第一部分是足迹图的构造函数对象FootPrintConstructer,第二部分是main主函数。

首先我们来进行第一部分FootPrintConstructer的编写,它接受一个参数period(代表K线周期)。在函数内部,创建了一个空对象self,并将该对象返回。这样,在使用FootPrintConstructer构造函数创建新对象时,可以通过访问对象的属性来操作和修改对象的数据。

接着我们创建self的属性和方法。首先我们利用Chart函数创建了一个图表对象,并将其赋值给self.c。在Chart图表中,第一个chart用于设置图表的一些基本样式和行为;plotOptions用于设置图表中绘图选项;tooltip用于设置图表的工具提示样式;series用于设置图表的数据系列;yAxis用于设置图表y轴(垂直方向)的样式和选项;navigation用于设置图表的导航按钮样式和选项。

在创建完图表后,代码调用self.c的reset方法来清空图表数据。

接下来是我们的重点,对于ticker数据的处理和图表数据的填充。首先初始化一些变量。其中,self.pre用来记录上一个数据,初始值为null;self.records用于存储K线数据,初始为空数组;这里定义feed方法,用于处理传入的数据,并更新图表和记录。首先,判断是否有上一个数据。如果self.pre为空,则将当前ticker赋值给self.pre。

这里为什么要使用self.pre先前数据呢,主要是为了判断买卖的动作。首先定义action为空。然后,根据最新数据的Last价格与上一个ticker数据的买价和卖价进行比较,确定买卖动作。如果最后价格大于等于上一个ticker卖价sell,则标记action为’buy’;如果最后价格小于等于上一个ticker的买价buy,则标记action为’sell’;

如果先前数据存在两跳以上间隙的情况下,当前last数据位于两跳的间隙,那么前两种条件就是不满足的。我们需要使用当前ticker的买价和卖价,如果当前数据最新价格大于等于最新的sell卖价,action定义为buy;如果最新价格小于当前买价,action定义为sell。当然,当前的买价和卖价也是可能存在两跳以上间隙的,如果最新的价格位于中间,那么这两个条件也是不符合的,我们定义action为both。表示同时存在买入和卖出。

当然我们也要考虑每日开盘,volume数据重置的情况 ,当判断最新的volume小于Volume的情况,需要把先前的pre.Volume设置为0。

当判断好action动作以后,接下来我们就要来判断amount的属性了。如果action标记不为空字符串,并且amount大于0,证明有交易正在发生,我们就要进行相应的逻辑处理。首先定义epoch变量,通过计算当前的时间戳并取整,这里是向下取整,我们要统计一根完整k线内ticker数据的详细变化。然后定义bar和pos变量,分别定义为null和undefined,这两个变量分布是我们画图需要的数据,和图表数据的更新参数。

如果records长度为0或者最新records时间戳小于epoch,代表k线更新,这个时候需要重置bar数据,将bar里的time属性更新为epoch,data属性清空,高开低收定义为当前最新价,然后把bar添加到records数组中。

重置bar以后,在当前k线进行的时候,我们需要对bar中的一些属性进行更新,将bar定义为当前k线数据,然后更新当前bar的最高价,最低价和收盘价,这里的pos是更新参数,在以前chart画图函数我们有提到过,当定义为-1的时候,表示最新的k线数据还没有固定,这时候就要不断的更新最新的k线数据。

这里的bar数据包括时间戳epoch,高开低收,表示k线的数据这里定义完成了。但是汇总的总体变化不能反应在这一根k线中,具体的ticker变化,以及对应的每个ticker价格对应的买方和卖方的成交量,我们将定义k线中具体ticker对应的成交量了,就是k线中这里要显示的数据,一个价格后面,是对应的卖方数量和买方数量。这个价格就是每一个ticker的最新价,ticker.Last,然后每一个最新价格对应的买方数量,和卖方数量,我们将这个数据存储在bar的data属性中。

所以这里我们首先进行初始化,如果bar.data[ticker.Last]未定义的话,为bar.data[ticker.Last]添加属性,将buy和sell数量分别定义为0。

还记得我们前面定义的action属性吗,这个时候根据价格变化判断出来的action,我们使用到这里判断amount的具体属性。这里有一种特殊情况,当action为both的时候,将买入和卖出的数量分别定义为当前的amount除以2。然后我们统计sellVol卖方数量,和buyVol数量,初始值定义为0,接着使用for循环进行累加获取各自方向的总和。这里使用tips保存生成的文本内容,这里我们来定义第一行内容,当前k线发生的总交易量,使用买方总量加上卖方总量。

接着我们来定义各个价格级别上的买卖对比信息,通过Object.keys(bar.data)将bar对象中的键放到一个数组中。然后,使用.sort()对数组进行排序,并使用.reverse()颠倒数组中的顺序,就是倒序排列。接下来,使用.forEach()方法对数组中的每个元素执行一个函数操作。这个函数接受一个参数p,表示当前遍历到的数组元素。在函数内部,通过p作为键访问bar.data对象,获取对应键p下的sell和buy属性的值,分别赋给变量pSell和pBuy。

接着,使用if语句判断pSell和pBuy的大小关系。如果pSell大于pBuy,说明卖方数量大于买方数量,表示价格下跌,将箭头符号’ ▼ ‘赋值给变量arrow。如果pSell小于pBuy,说明买方数量大于卖方数量,表示价格上涨,将箭头符号’ ▲ ‘赋值给变量arrow。如果pSell等于pBuy,表示买卖数量相等,将菱形符号’ ♦ '赋值给变量arrow。

这样就可以将每个价格级别p、买方数量pBuy、箭头符号arrow和卖方数量pSell拼接成一行字符串,并添加到tips变量中。我们还要显示买方数量和卖方数量的差值,通过这一行进行定义。这样呢,我们的足迹图数据就定义完成了。最后添加到self.c图表中,这里的数据包括k线图的时间戳,高开低收,还有足迹图数据tips,最后的pos代表图表的更新参数。当一个ticker数据处理完成以后,需要将当前ticker定义为self.pre。这样足迹图构造函数FootPrintConstructer就定义完成了。

在主函数中,搭建固定的框架,然后在while循环体外,使用FootPrintConstructer构造函数,创建footPrint对象,参数填写60000,也就是一分钟。在while循环体内,设置合约,获取ticker数据,如果成功获取到Tick数据,使用footPrint的feed方法开始处理数据,创建足迹图。在模拟回测的时候,这里要选择实盘级tick,不然这里出现的足迹图是不能反应真实的tick成交量的。

足迹图交易策略

下面我们来看使用足迹图进行交易的策略。理论上成交量是先行于价格的,买卖双方成交量的多少是原因,价格的变化是结果。所以在大多数情况下,量增价涨是一种常态,大部分K线都保持这种规律,而买卖均衡与价格背离却是一种偶然。接下来我们利用买卖均衡与价格背离这种现象,来尝试进行交易策略的构建。以下是策略逻辑:

  • 多头开仓:如果当前无持仓,并且收盘价大于开盘价,但是主动买量小于主动卖量
  • 空头开仓:如果当前无持仓,并且收盘价小于开盘价,但是主动买量大于主动卖量
  • 多头平仓:如果有多头持仓,并且利润超过100(止盈),或者损失超过-100(止损)。
  • 空头平仓:同样如果有空头持仓,当利润超过100,或者损失超过-100进行平仓。

我们在原有的代码基础上继续进行补充就可以,首先需要在feed方法上面定义arr空数组,lastTime为0;然后在进行图表绘图以后,使用push方法将一个对象添加到数组arr中,该对象包含了开盘价、收盘价和买卖量差。如果数组arr的长度大于2,则使用arr.shift()方法移除数组的第一个元素,以保持数组的长度为2。使用exchange.GetPosition()方法获取当前持仓信息,并初始化持仓量holdAmount和盈利金额profit的变量。

如果存在持仓信息,则根据持仓类型(多头或空头),更新持仓量holdAmount的值为正值还是负值,并将盈利金额profit赋值为当前持仓的盈利金额。如果当前K线的时间与上一次记录的时间不同,则表示进入了新的K线周期。

首先将lastTime定义为bar.time,然后根据数组arr中保存的最新数据,获取买卖量差volDiff,开盘价lastOpen、收盘价lastClose,利用收盘价减去开盘价的差作为价格差priceDiff。

接下来就到了交易信号的判断和具体交易操作的执行了,这里我们使用交易类库,设置单品种管理对象p:

  • 如果没有持仓且出现正的价格差和负的买卖量差,则打印信息并执行多头开仓操作。
  • 如果没有持仓且出现负的价格差和正的买卖量差,则打印信息并执行空头开仓操作。
  • 如果持有多头仓位且盈利金额超过100或亏损金额超过-100,则打印信息并执行多头平仓操作。
  • 如果持有空头仓位且盈利金额超过100或亏损金额超过-100,则打印信息并执行空头平仓操作。
/*backtest
start: 2023-09-19 09:00:00
end: 2023-09-26 15:00:02
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
mode: 1
args: [["symbol","rb888"]]
*/

var p = $.NewPositionManager()

var FootPrintConstructer = function(period) {
    
    var self = {} // 创建一个对象

    self.c = Chart({ // 创建Chart图表
        chart: {
            zoomType: 'x', // 缩放类型,设置为'x'表示只能在横向上进行缩放
            backgroundColor: '#272822', // 背景颜色
            borderRadius: 5, // 边框圆角大小
            panKey: 'shift', // 按住shift键可平移图表
            animation: false, // 是否开启动画效果
        },
        plotOptions: {
            candlestick: {
                color: '#00F0F0', // 蜡烛图的颜色
                lineColor: '#00F0F0', // 蜡烛图线条的颜色
                upColor: '#272822', // 上涨蜡烛图的颜色
                upLineColor: '#FF3C3C' // 上涨蜡烛图线条的颜色
            },
        },
        tooltip: {
            xDateFormat: '%Y-%m-%d %H:%M:%S, %A', // x轴(时间)的格式
            pointFormat: '{point.tips}', // 每个数据点的显示格式,{point.tips}表示从数据点的tips属性获取内容
            borderColor: 'rgb(58, 68, 83)', // 边框颜色
            borderRadius: 0, // 边框圆角大小
        },
        series: [{
            name: exchange.GetName(), // 获取交易所名称
            type: 'candlestick', // 蜡烛图
            data: [] // 空数组
        }],
        yAxis: {
            gridLineColor: 'red', // 网格线颜色
            gridLineDashStyle: 'Dot', // 网格线样式,Dot表示虚线
            labels: { // 标签样式,文本颜色
				style: {
					color: 'rgb(204, 214, 235)'
				}
			}
        },
        navigation: { //设置buttonOptions,包括按钮大小、符号的大小和位置、符号的边框宽度等
			buttonOptions: {
				height: 28,
				width: 33,
				symbolSize: 18,
				symbolX: 17,
				symbolY: 14,
				symbolStrokeWidth: 2,
			}
		}
    })
    self.c.reset() // 清空图表数据

    self.pre = null // 用于记录上一个数据
    self.records = []
    arr = []
    lastTime = 0 
    self.feed = function(ticker, symbol) {
        if (!self.pre) { // 如果上一个数据不为真
            self.pre = ticker // 赋值为最新数据
        }
        var action = '' // 标记为空字符串
        if (ticker.Last >= self.pre.Sell) { // 如果最新数据的最后价格大于等于上一个数据的卖价
            action = 'buy' // 标记为buy

        } else if (ticker.Last <= self.pre.Buy) { // 如果最新数据的最后价格小于等于上一个数据的买价
            action = 'sell' // 标记为sell

        } else {
            if (ticker.Last >= ticker.Sell) { // 如果pre.Buy和pre.Sell存在两跳间隙的情况下,ticker.Last处于中间,最新数据的最后价格大于等于最新数据的卖价 
                action = 'buy' // 标记为buy

            } else if (ticker.Last <= ticker.Buy) { // 如果pre.Buy和pre.Sell存在两跳间隙的情况下,ticker.Last处于中间,最新数据的最后价格小于等于最新数据的买价
                action = 'sell' // 标记为sell

            } else { //如果ticker.Buy和ticker.Sell存在两跳间隙的情况下,ticker.Last处于中间
                action = 'both' // 标记为both
 
            }
        }
        // reset volume 重新开盘重置
        if (ticker.Volume < self.pre.Volume) { // 如果最新数据的成交量小于上一个数据的成交量
            self.pre.Volume = 0 // 把上一个数据的成交量赋值为0
        }
        var amount = ticker.Volume - self.pre.Volume // 最新数据的成交量减去上一个数据的成交量
        if (action != '' && amount > 0) { // 如果标记不为空字符串,并且amount大于0,证明有交易正在发生
            var epoch = parseInt(ticker.Time / period) * period // 计算K线时间戳并取整
            var bar = null
            var pos = undefined //画图更新参数
            if (
                self.records.length == 0 || // 如果K线长度为0或者最后一根K线时间戳小于epoch
                self.records[self.records.length - 1].time < epoch
            ) {
                //Log('重置bar')
                bar = {
                    time: epoch,
                    data: {},
                    open: ticker.Last,
                    high: ticker.Last,
                    low: ticker.Last,
                    close: ticker.Last
                } 
                self.records.push(bar) // 把bar添加到records数组中
            } else {
                //Log('更新bar')
                bar = self.records[self.records.length - 1] // 当前k线数据
                bar.high = Math.max(bar.high, ticker.Last) // 当前K线的最高价与最新数据最后价格的最大值
                bar.low = Math.min(bar.low, ticker.Last) // 当前K线的最低价与最新数据最后价格的最小值
                bar.close = ticker.Last // 最新数据的最后价格
                pos = -1
            }
            if (typeof bar.data[ticker.Last] === 'undefined') { // 如果数据为空
                bar.data[ticker.Last] = { // 添加属性
                    buy: 0,
                    sell: 0
                }
            }
            if (action == 'both') { // 如果标记等于both
                bar.data[ticker.Last]['buy'] += amount/2 // buy累加
                bar.data[ticker.Last]['sell'] += amount/2 // sell累加
            } else {
                bar.data[ticker.Last][action] += amount // 标记累加
            }
            
            var sellVol = 0
            var buyVol = 0
            for (var i in bar.data) {
                sellVol += bar.data[i].sell
                buyVol += bar.data[i].buy
            }
            tips = '<b>◉ ' + (sellVol + buyVol) + '</b>'
            Object.keys(bar.data) // 将对象里的键放到一个数组中
                .sort() // 排序
                .reverse() // 颠倒数组中的顺序
                .forEach(function(p) { // 遍历数组
                    pSell = bar.data[p].sell
                    pBuy = bar.data[p].buy
                    if (pSell > pBuy) { // 卖方数量大于买方数量,下跌
                        arrow = ' ▼ '
                    } else if (pSell < pBuy) { // 买方数量大于卖方数量,上涨
                        arrow = ' ▲ '
                    } else {
                        arrow = ' ♦ '
                    }
                    tips += '<br>' + p + ' → ' + pBuy + arrow + pSell
                })
            tips += '<br>' + '<b>⊗ ' + (buyVol - sellVol) + '</b>'
            
            self.c.add( // 添加数据
                0, {
                    x: bar.time,
                    open: bar.open,
                    high: bar.high,
                    low: bar.low,
                    close: bar.close,
                    tips: tips
                },
                pos
            )

            arr.push({
                'open': bar.open,
                'close': bar.close,
                'diff': buyVol - sellVol
            })
            if (arr.length > 2) {
                arr.shift()
            }
            var position = exchange.GetPosition()
            var holdAmount = 0
            var profit = 0
            if (position.length > 0) {
                if (position[0].Type == PD_LONG || position[0].Type == PD_LONG_YD) {
                    holdAmount = position[0].Amount
                } else {
                    holdAmount = -position[0].Amount
                }
                profit = position[0].Profit
            }
            if (bar.time != lastTime) {
                lastOpen = arr[0].open
                lastClose = arr[0].close
                diff = arr[0].diff
                lastTime = bar.time
                priceDiff = lastClose - lastOpen
                volDiff = diff
                if (holdAmount == 0 && priceDiff > 0 && volDiff < 0) {
                    Log('多开')
                    p.OpenLong(symbol, 1)
                }
                if (holdAmount == 0 && priceDiff < 0 && volDiff > 0) {
                    Log('空开')
                    p.OpenShort(symbol, 1)
                }
                if (holdAmount > 0 && (profit > 100 || profit < -100)) {
                    Log('多平')
                    p.CoverAll()
                }
                if (holdAmount < 0 && (profit > 100 || profit < -100)) {
                    Log('空平')
                    p.CoverAll()
                }
            }
        }
        self.pre = ticker // 重新赋值
    }
    return self // 返回对象
}

function main() {
    var footPrint = FootPrintConstructer(60000) // 创建一个对象
    while (true) { // 进入循环模式
        if(exchange.IO("status")){
            LogStatus("行情和交易服务器连接成功, " + _D())
            exchange.SetContractType(symbol)
            var ticker = exchange.GetTicker() // 获取交易所Tick数据
            if (ticker) { // 如果成功获取到Tick数据
                footPrint.feed(ticker, symbol) // 开始处理数据
            }
            Sleep(3000)
        }else{
            LogStatus("正在等待与交易服务器连接, " + _D())
            Sleep(3000)
        }
    }
}

这样就完成了交易策略代码部分的编写,我们使用螺纹钢主力合约回测一下,这里设置回测的日期为最近的一周。根据回测的结果显示,我们一共进行了55笔交易,其中平仓盈亏是630元,但是手续费占据了很大部分,最后的预估收益为161元。

以上呢,就是足迹图的构建和使用足迹图进行的交易策略。其实足迹图可以使用的地方还有很多,大家可以尝试探索一下,有疑问的话,可以留言评论区,我们会热心解答!

视频参考链接: 《CTA策略之orderflow订单流策略(2)》

13.商品期货仓位管理工具:马丁策略

今天我们不谈策略,来讲一下商品期货交易中的仓位管理。初入期货市场的我们,总是希望把把盈利,开始的时候,总是以轻仓试探,当期货软件的数字一旦变红,我们马上就卖出;当尝到盈利甜头的我们,开始后悔为什么只下了一手,于是我们开始加大杠杆,梭哈进场,这时候的盈利和亏损都是成倍放大的。运气不好,亏损出现。这时候的我们又开始后悔,为什么要梭哈入场,明明可以轻仓,拥有更多时间等待价格回归,到如今我们的选择只有两条,割肉或者补仓。这时候,仓位管理的重要性就凸显出来。

毕竟每个量化交易系统不是完美的,可以对每一笔未来的方向有着准确的判断。不同的交易策略适用于不同的行情,比如上节课我们讲到的网格策略,就是特别适合于震荡的行情,当单边行情出现的时候,如果我们不及时止损,结果可能只有爆仓。所以我们的交易系统在按照交易逻辑运行策略的同时,还需要对资金和仓位做一些更好的准备和管理。

最近在知乎上看到这样的提问,大概率加上仓位管理,可以保持长期盈利吗?题主通过统计分析,构建出了一个正期望策略进行开仓的同时,准备使用马丁策略管理仓位,回答的答案也很是直白,马丁策略的最终结果是爆仓,马丁策略永远不要使用。今天呢,我们就来了解一下这个神奇的仓位管理工具,马丁策略。

马丁格尔策略最早起源于18世纪的法国,不过那个时候它多被用于赌桌上面,之后没过多久就在欧洲广为人知。在理论金融学里面,它有一个更熟悉的名字,鞅。理论上这是一种胜率接近于100%的策略,直到现在在很多交易市场都有它的身影,如:外汇、期货及数字货币市场。然而它真地是万能的吗?我们首先来了解下它的原理。

其实马丁格尔既不是交易策略,也不是交易机制,而是一种资金管理方式。其原理很简单:交易者每次亏损一定额度,就把下一次下单量加倍,直到盈利时把下单量恢复到初始值。如此一来,只需要盈利一次,不仅可以收回之前的亏损,还能获得第一次下单量的收益。显而易见这是一个逆势翻倍加仓的资金管理方式。

现假设有一枚正反两面一样重的硬币,不断的抛硬币,出现正面和反面的概率约等于50%,接下来我们用抛这枚硬币打赌,最初的下注金额是1元,如果出现正面赢1元,如果出现反面赔1元。理论上,硬币出现正反的概率是一样的,因为每次出现的结果相互独立不受影响,即50%。

根据马丁策略原理,每次赔钱时就把下注金额调整为上次下注金额的2倍,只需要赢一次就可以挽回之前的所有损失。但当连续亏损时,也将会输得一无所有。如果本金只有10元,第一次下注1元,出现反面亏损1元,账户余额为9元;第二次下注2元,出现反面亏损2元,账户余额为7元;第三次下注4元,出现反面亏损4元,账户余额为3元;这时就没有足够的资金下注了。

期货市场与赌场是不同的,期货的涨跌并不是完全随机赌大小,真实的金融交易市场要比赌场更加复杂。如果将马丁格尔策略用在期货交易中,一旦市场按照反方向趋势行情运行,后面随着行情的发展,头寸翻倍增加会越来越大,风险也随之加大。本节课呢,我们就来使用马丁策略应用于商品期货市场,看看是真的战无不胜还是爆仓离场。

我们首先来编写一个简单的马丁格尔策略,今天我们来使用sublime编辑器进行编写策略。最近有同学联系我们,说好不容易在网页编辑器写好的策略,结果因为忘记保存,结果全部代码丢失。其实呢,优宽也是支持在远程编辑器编写策略,并同步到优宽策略库里的。我们可以这样设置,首先我们需要编写一个策略,可以是空白的,然后保存;接着点击进入,右上角我们下载这个策略到本地;下载完成,这个策略用sublime打开,接着回到网页界面,点击远程编辑,需要安装一个插件。这里我们选择sublime plugin,下载这个插件到本地;然后点击进入sublime的packages,复制进入到库中。然后回到页面,点击更新密钥。复制这个密钥到本地策略的第一行,然后我们就可以在本地编辑器写策略了,点击保存,可以发现同步保存到我们的网页编辑器里了。这样就可以实现远程的策略编辑。

/*backtest
start: 2023-05-01 00:00:00
end: 2023-08-01 10:01:48
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/

var profits = 10
var unit = 1
var bei = 2

function main() {
    exchange.SetContractType(symbol)
    while (true) {
        var depth = exchange.GetDepth();
        if (!depth) return;
        var ask = depth.Asks[0].Price;
        var bid = depth.Bids[0].Price;
        var position = exchange.GetPosition()

        if (position.length == 0) {
            var redom = Math.random()
            if (redom < 0.5) {
                exchange.SetDirection("sell")
                exchange.Sell(bid, unit, "开空")
            }
            if (redom > 0.5) {
                exchange.SetDirection("buy")
                exchange.Buy(ask, unit, "开多")
            }
        }
        if (position.length > 0) {
            var type = position[0].Type;
            var profit = position[0].Profit;
            var amount = position[0].Amount;
            if (type == PD_LONG || type == PD_LONG_YD) {
                if (profit > profits) {
                    exchange.SetDirection("closebuy")
                    exchange.Sell(bid, amount, "多头止盈,当前盈利:" + profit)
                    unit = 1
                }
                if (profit < -profits) {
                    unit = unit * bei
                    exchange.SetDirection("buy")
                    exchange.Buy(ask, unit, "多头加仓,当前盈利:" + profit)
                }
            }
            if (type == PD_SHORT || type == PD_SHORT_YD) {
                if (profit > profits) {
                    exchange.SetDirection("closesell")
                    exchange.Buy(ask, amount, "空头止盈,当前盈利:" + profit)
                    unit = 1
                }
                if (profit < -profits) {
                    unit = unit * bei
                    exchange.SetDirection("sell")
                    exchange.Sell(bid, unit, "空头加仓,当前盈利:" + profit)
                }
            }
        }
        Sleep(1000 * 60 * 60 * 24)
    }
}

在策略开头,我们首先定义一些变量:profits表示盈利目标,我们设置为10,当盈利大于10的时候,我们进行止盈,而当盈利小于-10的时候,我们需要进行加仓;至于具体加仓的数量,需要根据初始的交易数量,逐步乘以加仓的倍数,这里设置初始的unit是1,bei表示加仓倍数,是2,表示每次加仓的数量是当前持仓数量的两倍。

在main函数中,首先设置交易合约类型,这里我们设置了外部的参数,方便使用不同的合约进行测试。

进入主循环,获取市场深度信息。

对于马丁开仓的方向,这里我们决定使用抛硬币模拟随机数的方式。如果当前没有持仓(position.length == 0),则根据一个随机数生成的值来决定开仓方向。如果随机数小于0.5,则设置交易方向为卖出(开空),并以当前买一价(bid)卖出指定数量的合约。如果随机数大于0.5,则设置交易方向为买入(开多),并以当前卖一价(ask)买入指定数量的合约。

对于离场,这里我们只有止盈离场,不存在止损离场。当出现盈利为负的时候,我们按照倍数进行加仓。如果当前有持仓(position.length > 0),就要根据持仓的类型判断操作。如果是多头持仓,就要判断当前盈利是否达到设定的盈利目标。如果盈利超过盈利目标,则设置交易方向为平多(卖出),以当前买一价卖出持仓数量的合约,并将单位数量unit重置为1。如果盈利低于负的盈利目标,则将单位数量乘以加仓倍数,设置交易方向为买入(加仓),以当前卖一价买入加仓数量的合约。

如果持仓类型为空头,则与多头持仓相反的操作。

最后,通过Sleep函数设定间隔时间,这里设置为每天执行一次交易操作。

这样一个简单的马丁策略就设置好了。我们点击保存,可以发现网页页面更新上了最新编写的策略。我们回测运行一下,时间设置为今年的5月到8月。

从回测结果可以看到,确实没有亏损,最后的盈利是35168元。但是收益曲线并不像是传说的那样曲线上升。我们看下回测日志,看下最大加仓次数,结果发现,最大加仓次数是是6,一共持有128个仓位,然后保证金不足就无法开仓了,确实冒了很大的风险。我们的初始资金是100W,燃油的保证金还算是比较低,当我们选择原油或者铁矿石合约的话,爆仓的情况相信会更容易发生。

这里提醒一下,我们我们每次开仓的选择是随机的方向,所以模拟回测的结果可能会不一致,我们再次运行一下。很不幸,这次最后的结果是亏损。

当然还有别的同学会有疑问,既然亏损加仓的时候容易出现爆仓,那我盈利加仓的时候可以吗,当亏损一旦出现,我就立即离场。这其实就是反向马丁策略的设计原理。我们在原有策略的基础上,可以改动试下。这里我们添加一个全局变量,preprofit,初始值为0。然后在这里当今日的盈利profit减去昨日的盈利preprofit大于设置的盈利阈值profits的时候,我们进行加倍的开仓;而一旦最新的盈利小于昨日的盈利,我们就要及时的进行止损。对于多头和空头,这里的处理逻辑是一样的。

/*backtest
start: 2023-05-01 00:00:00
end: 2023-08-01 10:01:48
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/

var unit = 1
var profits = 10
var bei = 2
var preprofit = 0

function main() {
    exchange.SetContractType(symbol)
    while (true) {
        var depth = exchange.GetDepth();
        if (!depth) return;
        var ask = depth.Asks[0].Price;
        var bid = depth.Bids[0].Price;
        var position = exchange.GetPosition()

        if (position.length == 0) {
            var redom = Math.random()
            if (redom < 0.5) {
                exchange.SetDirection("sell")
                exchange.Sell(bid, unit, "开空")
            }
            if (redom > 0.5) {
                exchange.SetDirection("buy")
                exchange.Buy(ask, unit, "开多")
            }
            preprofit = 0
        }
        if (position.length > 0) {
            var type = position[0].Type;
            var profit = position[0].Profit;
            var amount = position[0].Amount;
            if (type == PD_LONG || type == PD_LONG_YD) {
                if (profit - preprofit > profits) {
                    unit = unit * bei
                    exchange.SetDirection("buy")
                    exchange.Buy(ask, unit, "多头加仓,当前盈利:" + profit)
                }
                if (profit - preprofit < -profits) {
                    exchange.SetDirection("closebuy")
                    exchange.Sell(bid, amount, "多头止损,当前盈利:" + profit)
                    unit = 1
                }
            }
            if (type == PD_SHORT || type == PD_SHORT_YD) {
                if (profit - preprofit > profits) {
                    unit = unit * bei
                    exchange.SetDirection("sell")
                    exchange.Sell(bid, unit, "空头加仓,当前盈利:" + profit)
                }
                if (profit - preprofit < -profits) {
                    exchange.SetDirection("closesell")
                    exchange.Buy(ask, amount, "空头止损,当前盈利:" + profit)
                    unit = 1
                }
            }
            preprofit = profit
        }
        Sleep(1000 * 60 * 60 * 24)
    }
}

另外,还需要注意一点的是,当我们开仓完成,我们需要重置preprofit为0,方便持有盈利的判断。同样的使用燃油为目标合约,选择相同的时间段进行回测,可以看到,这时候确实不存在爆仓的风险了,只不过收益曲线下跌的很流畅。其实也可以理解,因为我们开仓的成功率是一半一半,而当盈利出现,我们继续选择加仓,在很多情况下,利润都会回撤,我们选择止损平仓,所以策略的收益比较不理想。

这节课我们选择使用马丁策略作为反面教材,验证了我们常挂在嘴边的“浮亏加仓”和“浮盈加仓”。但是无论是浮亏加仓还是浮盈加仓,都需要谨慎考虑仓位管理因素。马丁策略在一定程度上,可以定义为扛单,重点是我们有多少的资本去应对亏损,当我们在亏损的时候,继续选择去加仓,如果等不到趋势反转,这在一定情况下,确实会造成爆仓。所以我们在进行手动交易和量化交易的时候,对于仓位和资金的风险管理应该放在首要的位置。

对于仓位管理方面,以上有几个因素大家可以参考下:

资金管理:基于账户资金和交易目标,设定合理的头寸规模和资金使用计划,避免过度杠杆和过大的头寸。

风险控制:设置止损和止盈规则,控制亏损和保护盈利。合理控制仓位规模和加仓倍数,避免过度风险和波动性。

多样化投资组合:根据市场和行业的不同情况,可以分散投资于不同品种或不同策略,降低单一风险。

定期回顾和优化:定期检查和评估仓位管理和交易策略的效果,根据市场变化做出相应的调整和优化。

当然这些理论的建议大家都了解。我们都知道,相信一个理性人,确实厌恶亏损是我们的本能。如果在手动交易中,我们确实无法做到理性的割肉。那么在量化策略中,我们可以做到更系统的仓位和资金,用来确保长期盈利能力和资金安全。

视频参考链接:

《CTA策略之商品期货简易马丁格尔》

《图解正反马丁格尔策略》

14.商品期货自适应网格策略设计

在商品期货交易中,趋势跟踪、日内短线、手工炒单、套利、高频是大家比较常见的几种交易方法,但是除了这些方法之外,网格交易也是一种不错的选择,商品期货量化交易也是可以使用网格策略的,那具体怎么操作呢?本篇我们就用优宽量化(youquant.com)交易平台来实现一个简单的商品期货网格策略。

网格交易又称渔网交易,它也是量化交易的一种策略,简单的说,网格交易是利用行情震荡波动来赚钱的交易方法,在价格不断上下波动中,通过价格波动的上下区间布置网格低吸高抛获取利润。网格交易不依赖人的主观思考,完全是一种机械行为,比较适合用量化的方式进行交易,利用价格波动在网格区间内低买高卖,通过反复循环差价赚取利润,这种赚取差价的获利方式,可以参照下面这个图片:

网格交易本质上是一种空间变时间的玩法,其秉持的原则是“仓位策略比择时策略更重要”。简单的网格是以某个价位为基准点,当价格上涨戓下跌一定的点数或者一定的比例,挂N手数量空单戓多单,每一个格子即是盈利点位,但通常并不设置止损,当价格朝向与持仓有利的方向运动并达到网格点位时获利平仓,并且在该点位挂同样的买单戓卖单。这样这些交易订单就像渔网一样阵列,在行情的波动中来回盈利。

对于传统的网格策略,首先需要根据历史数据确定网格的上限和下限,然后根据品种的波动率情况设置网格的宽度,还要根据自己的资金实力设计网格的数量,网格数量越多,则需要加仓的点位就越多,以及计算好补仓资金份额,防止潜在的风险导致破网。

无论行情是上涨还是下跌,它可以平均开仓和平仓价格,这种交易方法不会增加风险,反而会降低风险。对于已经平仓的交易都是正收益,资金曲线相对稳定,这也是网格策略的优点之一。另外还有一个优点就是:网格策略不需要(要求)对市场方向做出正确的判断,这对于懒人或者对市场方向不是太敏感的交易者来说节省了很多时间和精力。

但是针对于商品期货市场,其巨大的风险性造成传统的网格策略在使用的时候,存在一定的缺陷:

(1)重复开仓和加仓风险:网格交易策略通常会在价格下跌时买入并设定不同的价格区间,而当价格连续下跌时,可能会造成频繁的买入操作,进而增加了持仓成本和风险。如果价格持续下跌,网格交易策略可能会导致不断加仓并承受更大的损失。

(2)无法适应剧烈行情:在剧烈行情的市场中,网格的间距可能会被迅速击穿,从而网格失效。

但是网格策略在商品期货市场中真的完全无法使用吗,当然不是。到目前为止呢,我们学习了很多的策略设计和仓位管理的知识,今天我们就来尝试一下改造原始的网格策略,看它能否变得温顺一点。

首先,我们来想一下这两个问题的解决方案,关于重复开仓和加仓的风险,如果大家看过前面的海龟交易策略,这个问题很好解决,我们可以设置一个最大持仓数量,当持仓数量达到上限,策略就不在开仓;

第二个问题,我们需要了解下网格设计的原理,在传统的网格策略中,网格区间是根据历史价格走势构建出来的,因此呢,存在一定的历史局限性,并且网格区间是在策略初始的时候构建


更多内容