使用JavaScript入门商品期货量化交易

Author: 雨幕(youquant), Created: 2023-08-15 17:04:31, Updated: 2024-09-12 09:04:45

一个管理机器人的机器人,教大家如何四折使用优宽量化。

这个功能是通过优宽平台扩展API,调用托管者启动和停止方法来实现的。作为一个专业开放的量化平台,优宽量化支持程序化调用平台的各项功能,所以开放了平台的扩展API接口。平台扩展API为我们提供了更大的自由度和灵活性,大家能够根据自己的需求对平台的一些功能进行扩展和定制化,进而提升用户体验和业务价值。

目前扩展API的开放的功能有: * GetNodeList: 返回请求中的API KEY对应的优宽量化交易平台账号下的托管者列表。

  • GetRobotGroupList: 返回实盘分组列表。

  • GetPlatformList: 返回交易所列表。

  • GetRobotList: 返回实盘列表。

  • CommandRobot: 该接口向指定实盘发送交互命令。

  • StopRobot: 停止运行指定实盘。

  • RestartRobot: 重启指定实盘。

  • GetRobotDetail: 获取指定实盘详细信息。

  • GetAccount: 返回账号的账户信息。

  • GetExchangeList: 返回支持的交易所列表以及需要的配置信息。

  • DeleteNode: 删除指定托管者节点。

  • DeleteRobot: 删除指定实盘。

  • GetStrategyList: 获取指定策略信息。

  • NewRobot: 根据参数设置创建实盘。

  • PluginRun: 使用扩展API接口调用调试工具功能。

  • GetRobotLogs: 获取指定实盘的日志信息。

这些都是扩展api支持的功能。大家也不需要特别的记忆,只需要知道有这些功能,具体的方法查询可以到api页面。这些功能可以帮助开发者对优宽量化交易平台进行更好地管理、监控和调试。开发者可以根据自己的需求使用这些API,定制化平台的功能和业务逻辑,实现更丰富的交易策略和用户体验。

今天我们就以定时开启和停止实盘为例,讲解一下平台扩展API的使用方法。

需要提前说明的是,这个策略的运行是不需要在实盘中的,使用自己的电脑在后台运行就可以。这里为了方便展示,我们在优宽量化编辑器里编写策略,然后运行在自己的电脑中就可以。 首先我们需要申请优宽量化API:

打开账号设置,选择API接口标签,然后点击右上方创建新的ApiKey,默认“*”号即开启了所有权限。指定具体接口权限,需要输入对应的扩展API函数名,使用逗号间隔,例如:GetRobotDetail,DeleteRobot。就会赋予这个API KEY调用获取实盘详细信息接口、删除实盘接口的权限。创建完毕后页面就会显示“AccessKey”和“SecretKey”。

在优宽量化的扩展API中,可以使用StopRobot()函数来停止正在运行的机器人,使用RestartRobot()函数来启动已经停止的机器人,但是在这之前需要请求指定的URL,以及对“secretKey”和“accessKey”进行md5加密操作,才能调用StopRobot()函数和RestartRobot()。具体的步骤这样的:

import hashlib
import requests
from datetime import datetime
import time
import JavaScripton


# 获取 API 请求参数
def getParam(version, ak, method, args):
    return {
        'version': version,
        'access_key': ak,
        'method': method,
        'args': JavaScripton.dumps(args),
        'nonce': int(time.time() * 1000)
    }

# 计算 MD5
def md5(param):
    paramUrl = f"{param['version']}|{param['method']}|{param['args']}|{param['nonce']}|{secretKey}"
    return hashlib.md5(paramUrl.encode('utf-8')).hexdigest()

# 获取最终请求 URL
def getFinalUrl(param):
    url = "https://www.youquant.com/api/v1?"
    return f"{url}access_key={accessKey}&nonce={param['nonce']}&args={param['args']}&sign={param['sign']}&version={param['version']}&method={param['method']}"

# 获取 API 信息
def getAPIInfo(method, dateInfo):
    param = getParam("1.0.0", accessKey, method, dateInfo)
    md5Result = md5(param)
    param['sign'] = md5Result
    finalUrl = getFinalUrl(param)
    info = requests.get(finalUrl).JavaScripton()
    return info

# 判断是否在交易时间段内
def isTrading():
    currentTime = datetime.now()
    currentHour = currentTime.hour
    currentMinute = currentTime.minute
    if (
        (currentHour >= 9 and currentHour < 11) or
        (currentHour == 11 and currentMinute <= 30) or
        (currentHour == 13 and currentMinute >= 30) or
        (currentHour >= 14 and currentHour < 15) or
        (currentHour >= 21 and currentHour < 23)
    ):
        return True
    else:
        return False


# 填入你的 secretKey、accessKey 和 botId
secretKey = ...
accessKey = ...
botId = ...


# 主函数
def main():
    while True:
        info = getAPIInfo('GetRobotDetail', [botId])
        if isTrading():
            if info['data']['result']['robot']['status'] == 4:
                getAPIInfo('RestartRobot', [botId])
                print('启动策略')
        else:
            if info['data']['result']['robot']['status'] == 1:
                getAPIInfo('StopRobot', [botId])
                print('停止策略')
        time.sleep(10)

if __name__ == "__main__":
    main()
  • 首先导入需求的包:
  • 函数getParam:用于获取API请求参数对象。接收四个参数:version表示API版本号,ak表示访问密钥,method表示API方法,args表示API方法的参数。函数返回一个包含版本号、访问密钥、方法、参数和时间戳的字典对象。
  • 函数md5:用于计算MD5签名。接收一个参数param,该参数是getParam函数返回的参数对象。函数首先将版本号、方法、参数、时间戳和密钥拼接成一个字符串,然后对该字符串进行MD5加密,并以十六进制的形式返回加密结果。
  • 函数getFinalUrl:用于获取最终的请求URL。接收一个参数param,该参数是getParam函数返回的参数对象。函数将基础URL和参数对象中的访问密钥、时间戳、参数和签名拼接起来,形成完整的请求URL,并返回该URL。
  • 函数getAPIInfo:用于获取API信息。接收两个参数:method表示API方法,dateInfo表示API方法的参数。函数首先调用getParam函数获取参数对象,然后调用md5函数计算签名。接下来调用getFinalUrl函数获取最终的请求URL。最后,使用requests库向指定的URL发送HTTP请求,并将返回的信息解析为JavaScriptON格式后返回。

重启和停止实盘是需要判断是否处于交易时间段内的,函数isTrading:用于判断当前是否处于交易时间。函数获取当前时间的小时和分钟,并根据一系列条件判断是否在交易时间段内。如果满足条件,则返回True表示处于交易时间段内,否则返回False表示不处于交易时间段内。具体的判断条件包括上午9点至11点半、下午1点半到3点之间、晚上9点至11点之间。

其中secretKey和accessKey变量需要修改为刚才申请优宽量化API“AccessKey”和“SecretKey”,注意是字符串类型。全局变量botId则是指定的机器人ID号,数字类型。

接着就要设置我们的主函数了。该策略一共有3个可以修改的全局变量,分别是:secretKey、accessKey、botId。其中secretKey和accessKey变量使我们刚才申请的优宽量化扩展API,全局变量botId则是指定的实盘ID号,注意填写的时候前两个是字符串类型。最后一个数字类型。如下面的代码:

在主函数中,调用函数 getAPIInfo() 获取关于机器人的详细信息,保存在变量 info 中。 调用函数 isTrading() 判断当前市场是否在交易。

如果市场正在交易,并且视频的状态为4(就是暂停状态),则调用 getAPIInfo的(‘RestartRobot’)方法 重启实盘,并输出日志’启动策略’。如果市场不在交易,并且实盘的状态为1(表示运行中状态),则调用 getAPIInfo的’StopRobot’方法停止实盘,并输出日志 ‘停止策略’。等待后继续下一次循环。

代码最后运行我们的主函数。我们保存这段代码,然后使用最简单的运行方法,将它运行在终端中,然后开始运行。现在处于实盘的运行时间,我们手动关闭实盘,可以看到在启停机器人,又为我们开启了实盘。

使用这种方式,我们就可以定时开启关闭实盘,而且不需要额外的花费。作为抛砖引玉,本节课讲解的策略只能管理一个机器人,不过相信你通读策略代码后,可以对该策略进行升级改进,可以升级为管理多个机器人,以及对优宽量化策略、托管者等诸多功能加以扩充。

使用这种方式,我们可以实现定时开启和关闭实盘,同时无需额外花费。作为抛砖引玉,本节课讲解的策略只能管理一个机器人,不过相信我们通读策略代码后,可以对该策略进行升级改进,可以升级为管理多个机器人。而且通过扩展api,我们也可以对优宽量化策略、托管者等功能进行更多的扩展和改进。这样可以更加灵活地管理和运行我们的实盘,提升整体策略的执行效率和功能性。

24:K线形态在策略中的应用:单K线形态

1990年,史蒂夫 · 尼森将古老的蜡烛图技术系统地介绍给了西方投资界,这一举动震惊了传统的技术分析方法,史蒂夫 · 尼森因此被誉为现代蜡烛图技术之父。蜡烛图不仅全球广泛普及,而且经久不衰,沿用至今。几乎在任何一个交易软件上都能看到它的身影,之所以如此流行,得益于其简单性和清晰性。

蜡烛线能够分割为不同的时段进行使用,不管是你想看1天、1小时、30分钟,都不成问题。蜡烛图用来描述特定时间内的价格波动状况。作为一段数据的容器。从最底层的Tick数据流开始,蜡烛线根据时间周期划分成一段一段,每个周期内的第一个价格就是开盘价,最后一个价格就是收盘价,周期内最高的那个价格就是最高价,周期内最低的那个价格就是最低价,每个容器里面都储存着开高低收、成交量、时间等数据。这就是蜡烛线,也就是k线的由来。

我们就来剖析一下蜡烛图,看看分别代表了什么?

  • 蜡烛线由一定时间以内的开盘价、收盘价、最高价和最低价组成;
  • 如果收盘价高于开盘价,我们称该蜡烛线为阳线,通常以红色蜡烛线表示,这里左边蜡烛线为阳线;
  • 如果收盘价低于开盘价,我们称该蜡烛线为阴线,通常以绿色蜡烛线表示,这里右边蜡烛线为阴线;
  • 蜡烛线的中空部分我们称为“实体”;
  • 蜡烛线上部分和下部分的细线我们称为影线;
  • 上影线的顶部为“最高点”;
  • 下影线的底部为“最低点”。

观察蜡烛线形态时可以同时考虑单个蜡烛线线和连续蜡烛线的情况。单个蜡烛线形态更多的是用于提供关于市场变化的一些信号,

比如这里的一些常见的形态:

Doji(十字线):开盘价与收盘价接近,没有实体或很小的实体。 Shooting Star(射击之星):上影线较长,实体同样没有或者很小。 Inverted Hammer(倒锤头线):上影线较长,实体较小。

观察连续蜡烛线时,可以关注以下一些常见的形态,连续蜡烛线形态则能揭示更多的趋势和模式。:

多头信号:如连续几个上涨的K线,或者多个看涨形态的组合。 空头信号:如连续几个下跌的K线,或者多个看跌形态的组合。 转变信号:比如这里的塔型顶形态、双顶/底形态等。

K线(蜡烛线)形态的判断存在主观性,不同的观察者可能会得出不同的结论。这可能导致在相同的数据上出现不一致的分析结果。因此可以使用标准化的技术分析库,确定各种不同形态的k线,这样可以在一定程度上减少人为主观因素对K线形态分析的影响。TA库通常提供了一系列已定义的技术指标和形态识别函数,包括各种常见的K线形态。通过使用这些标准化的函数,可以自动计算和识别K线的形态,减少个体观察者的主观偏见。

因此本课程将以单个K线和连续K线的形态,编写样例策略,来观察k线形态在策略中的应用。

单个K线的策略应用

首先我们来学习单个K线的形态在策略中的应用,其实单个k线的应用就是主要基于实体和影线的不同形态代表背后的多空趋势。

对于实体部分:

长阳线显示出价格的买盘强劲。阳线越长,价格收盘价就越高于开盘价。这意味着,价格自开盘价大幅走高,且买方力量强大。换句话说,市场多头力量超过空头,并最终推高价格。

长阴线显示价格卖盘强劲。阴线越长,价格收盘价就越低于开盘价。这意味着,价格自开盘价大幅下跌,且卖方力量强大。换一句话说,市场空头力量盖过多头,并最终推低价格。

对于影线部分:

上影线和下影线为交易者提供了重要的交易线索。

上影线显示出了价格的时段高点,而下影线则显示出了价格的时段低点。

蜡烛线越长,意味着买盘或卖盘越强。蜡烛线越短,意味着买盘或卖盘活动不强。

锤子线形态

本节课我们的第一个单k线示范形态是锤子线形态。

锤子线形态(Hammer Candlestick)是价格行为交易者在其职业生涯中最早学习的蜡烛图形态之一。从根本上说,锤子线形态是一种常见且非常强大的反转交易信号,通常可以暗示趋势的延伸或回撤阶段的结束。

锤子形态是单个k线模式。锤子线的技术形态是由K线实体较小而下影线较长的单根K线体现而出,K线的下影线会是K线实体部分的两倍以上,K线的上半部分处于实体部分,看起来K线就像一个锤子的形状。

下面是一个策略使用ta库判断锤子线形态作为多头入场的标志,并使用移动止盈止损类策略作为出场的标志,看一下锤子线形态是否真的有效。

/*backtest
start: 2023-01-04 09:00:00
end: 2023-07-11 15:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

// 移动止盈止损

function main() {
    var mp = 0;        // 持仓状态,0表示空仓,1表示持仓
    var buy_price = 0; // 开仓价格
    var takeProfitRatio = 0.005; // 初始盈利比例
    var stopLossRatio = 0.01;    // 初始止盈止损比例
    var p = $.NewPositionManager();
    var c = KLineChart();
    var symbol = 'SA888'

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!");

            exchange.SetContractType(symbol);
            var records = exchange.GetRecords();

            var longArr = talib.CDLHAMMER(records) // 调用talib库中的锤子线形态,返回数组

            var long = longArr[longArr.length - 2] // 获取上根K线形态数据


            if (mp == 0 && long > 0) {             // 出现锤子线形态,下多单
                Log('long',long)
                p.OpenLong(symbol, 1);
                pos = p.GetPosition(symbol, PD_LONG);
                buy_price = pos.Price;
                mp = pos.Amount;
            }

            var takeProfitPrice = buy_price * (1 + takeProfitRatio);   // 计算盈利价格
            var stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 计算止盈止损价格

            if (mp == 1 && records[records.length - 1].Close > takeProfitPrice) {
                Log("达到新的赢利点,更新比例和价格");
                takeProfitRatio += 0.005; // 增加盈利比例
                var takeProfitRatio = _N(takeProfitRatio);                // 格式化浮点数
                var takeProfitPrice = buy_price * (1 + takeProfitRatio);  // 更新盈利价格
                stopLossRatio -= 0.001;   // 更新止盈止损比例
                var stopLossRatio = _N(stopLossRatio);                     // 格式化浮点数
                var stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 更新止盈止损价格
                Log("更新盈利比例:",takeProfitRatio);
                Log("更新止盈止损比例:",stopLossRatio);
            }

            if (mp == 1 && records[records.length - 1].Close < stopLossPrice) {
                Log("达到止盈止损点,平仓并退出交易");
                p.Cover(symbol)
                var buy_price = 0; // 重置开仓价格
                var takeProfitRatio = 0.005;
                var stopLossRatio = 0.01;
                var mp = 0;
            }



            for (var i = 0; i < records.length; i++) {
                var bar = records[i];
                c.begin(bar);
                c.plot(long,{overlay: false});
                c.plotchar(long > 0, {char: '锤子线', size: "15px", overlay: true});
                c.close();
            }

            Sleep(1000)
        } else {
            LogStatus(_D(), "未连接CTP!");

            Sleep(1000)
        }
    }
}

不需要额外的语句来描绘和判断锤子线形态,只需要使用ta库就可以。talib.CDLHAMMER(records)会计算锤子线形态,返回数组,当返回数组中出现正值的时候,我们判断为呈现锤子线形态,然后作为多头入场的信号,然后使用移动止盈止损功能作为出场的信号。

这里的画图同样使用klinechart,由于我们想在k线上标注锤子线,所以可以plotshape函数。

我们开看一下回测图像,可以看到当判断出现锤子线形态的时候,k线确实有一个很短或者几乎没有的上影线,而下影线都是较长的状态。但是是否真的作为一个底部反转的标志,经过观察整体的走势,效果并不明显。只是抓住了一个短暂的市场反转。

根据回测结果,可以看到只利用蜡烛图技术中的锤子线形态,作为趋势反转判断的依据说服力并不强。这是因为单独使用锤子线形态作为买卖依据可能存在一定的局限性。锤子线形态的有效性受市场环境的影响。因此,需要考虑整体市场走势和趋势状态和其他指标,用来确定确定是否适合使用锤子线形态作为交易信号。

裸K上下影线在交易策略中的应用

如果你认为ta库的k线形态判断对于你来说,相当于一个黑盒子,不能确定具体的使用方法,其实我们也可以利用k线上影线,实体和下影线的关系,自行编造函数去判断市场的形态。

一般情况下,上影线越长,证明阻力就越大,多头即将由强势转变为弱势,未来价格可能会回调或者下跌。反之下影线越长,证明支撑力就越大,空头即将由强势转变为弱势,未来价格可能会反弹或者上涨。所以

经典的K线理论告诉我们,在众多的K线形态图中,如果出现较长的上影线或下影线,就是市场即将要发生转势的时候,这也是判断市场趋势转变的重要参考之一。

总体来讲,长上影线和下影线出现的概率都是较低的,一旦出现就说明市场当时最强烈的趋势走向可能发生逆转,后市转而下跌的概率较大。不过在使用影线判断行情的时候,需要有个前提,那就是市场之前已经经历过较大的涨幅或者跌幅,积累了较大的能量。

根据上面的K线理论,我们就可以试着开发一个长上下影线的策略:

第一步:计算上影线长度UP

第二步:计算实体长度MIDDLE

第三步:计算下影线长度DOWN

多头开仓:下影线长度大于实体加上影线的和的BN倍

空头开仓:上影线长度大于实体加下影线的和的SN倍

多头平仓:上影线长度大于实体加下影线的和的SN倍

空头平仓:下影线长度大于实体加上影线的和的BN倍

注意,这里的BN和SN是系数,因为相对来说期货通常在上涨时涨的缓,下跌时跌的急,所以我们在做多或做空时分别给与不同的系数。

下面我们来编写代码,这里的重点是上影线长度UP,实体长度MIDDLE和下影线长度DOWN的计算。因为会出现阳线(收盘价大于开盘价)和阴线(收盘价小于开盘价),所以三个长度计算需要使用三元表达式。

function main() {

    var mp = 0;                     // 持仓标志,初始值为0
    var p = $.NewPositionManager(); // 创建仓位管理器对象
    var c = KLineChart();           // 创建K线图表对象

    var symbol = 'SA888'

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接CTP !");
            exchange.SetContractType(symbol);
            var records = exchange.GetRecords();

            if (!records || records.length < 2) { // 等待获取足够多的K线数据
                Sleep(1000);
                continue;
            }

            var record = records[records.length - 2]; // 获取倒数第二根K线记录

            var UP = record.High - (record.Close > record.Open ? record.Close : record.Open); // 上影线
            var MIDDLE = (record.Close > record.Open ? record.Close : record.Open) - (record.Close > record.Open ? record.Open : record.Close); // K线实体
            var DOWN = (record.Close > record.Open ? record.Open : record.Close) - record.Low; // 下影线


            if (mp == 0 && DOWN > (MIDDLE + UP) * BN) { // 开多仓条件
                p.OpenLong(symbol, 1); // 开多仓
                mp = 1; // 更新持仓标志
            }

            if (mp == 0 && UP > (MIDDLE + DOWN) * SN) { // 开空仓条件
                p.OpenShort(symbol, 1); // 开空仓
                mp = -1; // 更新持仓标志
            }

            if (mp == 1 && UP > (MIDDLE + DOWN) * SN) { // 平多仓条件
                p.Cover(symbol); // 平多仓
                mp = 0; // 更新持仓标志
            }

            if (mp == -1 && DOWN > (MIDDLE + UP) * BN) { // 平空仓条件
                p.Cover(symbol); // 平空仓
                mp = 0; // 更新持仓标志
            }

            for (var i = 0; i < records.length; i++) {
                var bar = records[i];
                c.begin(bar);
                c.plotchar( UP > (MIDDLE + DOWN) * SN, { char: '长上', size: "10px", overlay: true });
                c.plotchar( DOWN > (MIDDLE + UP) * BN, { char: '长下', size: "10px", overlay: true });
                c.close();
            }

            Sleep(1000);

        } else {
            LogStatus(_D(), "未连接CTP !");
        }
    }
}

对于up值,第一个值high是确定的,第二个值当阳线的时候,是收盘价,否则当阴线的时候,是开盘价;middle值,对于阳线和阴线,要区分不同情况,同样使用三元表达式,确定算出来的值为正值;第三个down值,针对于阳线和阴线的不同情况,使用三元表达式进行区分。

接着我们定义市场的信号,当下影线长度大于上影线加实体的和的BN倍的时候,证明空头力量减弱,这个时候应该多头开仓或者空头平仓;而当上影线长度大于下影线长度加上实体的和的SN倍的时候,证明多方力量减弱,这个时候应该开平多或者开空。这里的BN和SN值不是固定的系数,可以进行调参。经过调参过后,针对于纯碱品种,开多平空BN是2,而开空平多SN的系数是1。

最后使用klinechart在图表中对于长上影线和长下影线进行标注。

我们来看下回测结果,从k线图可以看到,我们针对两种形态确实进行了明显的标注。而使用裸k针对于2023年上半年具有明显趋势的纯碱合约,确实取得了比较好的回测结果,但是当运用于实盘的时候,需要考虑不同的期货品种和市场环境,我们换一个期货品种,甲醇的主力合约,同一时间段的收益是负的,因此还是那句老话,实盘有风险,尝试需谨慎哈。

裸K简单直观,并且信息量丰富,是交易中最常见的基础数据和工具,长上下影线可以很好的衡量多头和空头的力量变化,并且其背后的原理也非常简单,这对初入门的量化新手非常友好。但是实战中的K线走势与理论并不会一模一样,也就是说我们在交易中追求的是一个大概率,而不是确定性。

可以总结发现,单一的k线形态无法完全捕捉市场的复杂性。应结合其他技术指标,如移动平均线、相对强弱指标(RSI)等,进行综合分析。这些指标可以提供额外的确认或过滤信号,增加交易决策的准确性。

25:K线形态在策略中的应用:多K线形态

本节课我们继续讲解K线形态在策略中的应用。除了使用单独的k线观察市场的反转,也可以通过观察多个K线形态的组合来判断市场的走势。

通过分析多个K线形态的组合构成,包括各种各样的形状和排列方式,可以提供更全面的市场走势判断。其中包括使用两根k线的吞没形态和平头顶部和平头底部形态。而三k线形态包括早晨之星和黄昏之星,白三兵和三只乌鸦,和两阳吃一阴与两阴吃一阳等等。当然还有使用五日k线的Breakaway 脱离形态等。这些形态的出现可以提供关于市场走势的信号,例如趋势反转、继续延续等。在我们的api文档里面还有很多k线的不同形态,大家有需要可以自行查找使用。

另外一种是结合不同的技术指标,例如移动平均线、相对强弱指标(RSI)、随机指标(Stochastic)等。通过观察这些指标和k线形态的交叉、背离等现象,可以更好地了解市场的趋势和反转点。

多k线形态

K线形态是通过观察价格走势的图表模式来进行分析的,它是根据特定的价格信息形成的图形模版进行识别,对于机器学习熟悉的朋友一定很熟悉,Pattern Recognition模式识别。针对于多个k线组成的模式,我们试着探索一下多种形态背后代表的市场走势预测。

本节课我们使用的例子是吞没形态。 吞没形态也叫抱线形态,它是由2根K线组成的复合形态(如上图所示),国内很多交易者会以“阳包阴”或者“阴包阳”来表述吞没形态。其中“阳包阴”为上涨吞没形态,“阴包阳”为下跌吞没形态。从图上看上涨吞没形态是一根大的阳线包住了前面的阴线,下跌吞没形态是一根大阴线包住了之前的阳线。

吞没形态是市场状态表象,但实际却蕴含了交易者之间的心里和资金博弈。它预示着市场价格走向即将反转,在实际使用中吞没形态会给分析和交易起到非常好的效果。尤其是在进出场点位上,或者止盈止损点位上,吞没形态都可以做比较好的参考。

本策略就来使用吞没形态作为一个开仓和平仓交易的信号。

/*backtest
start: 2023-01-03 09:00:00
end: 2023-07-11 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

// 吞没形态
function main() {
    var mp = 0; // 持仓状态,0表示空仓,1表示持仓
    var p = $.NewPositionManager();
    var c = KLineChart();
    var symbol = 'SA888';

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!");

            exchange.SetContractType(symbol);
            var records = exchange.GetRecords();

            if (!records || records.length < 20) { // 等待获取足够多的K线数据
                Sleep(1000); 
                continue;
            }

            var atr = TA.ATR(records, 20)[records.length-2];

            var engulfeds = talib.CDLENGULFING(records);  //判断吞没形态
            var engulfed = engulfeds[engulfeds.length-2];

            if (mp == 0 && engulfed == 100 && atr > 30) { //空仓,上涨吞没开多
                p.OpenLong(symbol, 1);
                mp = 1;
            }

            if (mp == 0 && engulfed == -100 && atr > 30) { //空仓,下跌吞没开空
                p.OpenShort(symbol, 1);
                mp = -1;
            }

            if (mp == 1 && engulfed == -100) { //持多,下跌吞没平多
                p.Cover(symbol);
                mp = 0;
            }
            if (mp == -1 && engulfed == 100) { //持空,上涨吞没平空
                p.Cover(symbol);
                mp = 0;
            }
            

            for (var i = 0; i < records.length; i++) {
                var bar = records[i];
                var currentEngulfed = engulfeds[i];
                c.begin(bar);
                c.plotchar(currentEngulfed == 100, {char: '上涨吞没', size: "10px", overlay: true});
                c.plotchar(currentEngulfed == -100, {char: '下跌吞没', size: "10px", overlay: true});
                c.plot(currentEngulfed, '吞没指标');
                c.close();
            }

            Sleep(1000)
            
        } else {
            LogStatus(_D(), "未连接CTP!");

            Sleep(1000)
        }
    }
}

使用talib库进行吞没形态的判断。当出现上涨吞没的时候,吞没形态数组会返回正100,而当下跌吞没出现的时候,数组返回负100。

接着我们利用吞没形态进行开仓和平仓信号的判断。

当上涨吞没的时候,进行开多仓,而当下跌吞没的时候,进行平多仓; 与之相反的是,当下跌吞没的时候,开空仓,而当上涨吞没的时候,平掉我们的空仓。

另外,对于开仓,我们还有一个限制条件,就是atr。ATR 是一种技术指标,用于衡量价格波动的程度。它通常用于确定价格的波动幅度和设置止损/止盈水平。这里使用的atr周期是20。

这里我们增加了atr > 30,表示 Average True Range (ATR) 的值大于30。 在这个策略中,atr > 30的条件是为了过滤掉价格波动较小的情况,以避免在市场波动较小的时候频繁开仓。当前的ATR值大于30时,表示市场波动较大,可以考虑进行交易。

在代码最后,我们使用klinechart的plotchar函数,在吞没指标为100的时候,标注“上涨吞没”,而在指标为-100的时候,标注“下跌吞没”。

在回测分析中,在图表中可以看到,当出现上涨吞没信号时,表示今日的阳线完全包住了前一天的阴线;而当出现下跌吞没信号时,表示今日的阴线完全包住了前一天的阳线。这些信号被认为是市场价格反转的可能迹象。

然而,从整体上上看,这些信号有时候可能会出现的过于频繁,对应的收益图表中显示,尽管在某些情况下可能表现出较高的收益率,但是总体上显示收益的波动性较大,这意味着信号的稳定性较低。

这种不稳定性可能是由于多种因素导致的,例如市场环境的变化、特定交易策略的限制以及其他未考虑的因素。因此,在使用这些吞没信号作为交易决策依据时,需要谨慎,并结合其他技术指标和风险管理策略。

K线形态和技术指标的结合

这一部分我们来讲K线形态和技术指标的结合。

当我们结合K线形态和技术指标时,一个重要的概念是背离(Divergence)。背离是指价格走势与某个技术指标之间出现不一致的情况。它可以提供有关可能的市场转折点的信号。

在分析中,我们通常使用一种或多种技术指标来补充对价格走势的判断。例如,常用的技术指标有相对强弱指标(RSI)、移动平均线(MA)、随机指标(Stochastic Oscillator)等。

当价格走势形成新的高点或低点时,我们会观察技术指标是否呈现相应的高点或低点。如果价格走势形成了新的高点,但技术指标没有形成相应的高点,这就被称为“顶背离”(Bearish Divergence)。反之,如果价格走势形成了新的低点,但技术指标没有形成相应的低点,就被称为“底背离”(Bullish Divergence)。

具体来说,顶背离可以暗示着可能的市场顶部形成,表明买盘力量逐渐减弱,卖盘力量可能增强,市场可能转为下跌趋势。而底背离则可能暗示市场底部的形成,表明卖盘力量逐渐减弱,买盘力量可能增强,市场可能转为上涨趋势。

本节课我们可以以MACD指标和K线的背离为例进行讲解。

MACD指标(Moving Average Convergence Divergence)是我们的老朋友了,作为一种常用的趋势指标,它通过计算两个移动平均线之间的差异来显示价格动量的变化情况。MACD包括两条线,分别是快速线(MACD Line)和慢速线(Signal Line),以及一个柱状图。

快速线(MACD Line):快速线是MACD指标中的主要线条,也称为DIF线(Difference Line)。它通过计算短期移动平均线(通常是12日指数移动平均线)与长期移动平均线(通常是26日指数移动平均线)之间的差异而得出。快速线的数值反映了价格动量的变化情况。

慢速线(Signal Line):慢速线是MACD指标的辅助线条,有时也称为DEA线(Detrended Exponential Average)。它是对快速线进行平滑处理的结果,通常采用9日指数移动平均线来计算。慢速线的数值可以帮助判断价格走势的趋势以及买卖信号。

柱状图(Histogram):MACD柱状图是由快速线和慢速线之间的差值绘制而成的。柱状图的高低表示快速线与慢速线之间的差异程度。当快速线穿越慢速线向上时,柱状图呈现正值,可能表明买入信号;当快速线穿越慢速线向下时,柱状图呈现负值,可能表明卖出信号。

快速线(MACD Line):DIF线(Difference Line)。它通过计算短期移动平均线(通常是12日指数移动平均线)与长期移动平均线(通常是26日指数移动平均线)之间的差异而得出。

慢速线(Signal Line)DEA线(Detrended Exponential Average)。通常采用9日指数移动平均线来计算。

柱状图(Histogram):MACD柱状图是由快速线和慢速线之间的差值绘制而成的。

当MACD指标与K线形态出现背离时,可能会给出一些有意义的信号。背离的类型可以是底背离或顶背离,具体含义如下:

底背离(Bullish Divergence):当价格的低点形成新的低点,但MACD指标由负转正,就发生了底背离。这可能表明价格下跌动力的减弱,市场可能即将出现回升的机会。

顶背离(Bearish Divergence):当价格的高点形成新的高点,但MACD指标由正转负,就发生了顶背离。这可能表明价格上涨动力的减弱,市场可能即将出现下跌的机会。

注意:底背离和顶背离的判断方法有很多,大家可以根据自己的交易理念进行选择~~

在顶背离情况下,卖盘可能会接管市场控制权,因此平多开空符合这种预期。

在底背离情况下,买盘可能会接管市场控制权,因此平空开多符合这种预期。

我们根据这个思路来编写一下代码。

/*backtest
start: 2022-07-18 09:00:00
end: 2023-07-24 15:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

function main() {
    var mp = 0; // 持仓状态,0表示空仓,1表示持仓
    var p = $.NewPositionManager();
    var c = KLineChart();
    var symbol = 'FG888';

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!");

            exchange.SetContractType(symbol);
            var records = exchange.GetRecords();

            if (!records || records.length < 26) { // 等待获取足够多的K线数据
                Sleep(1000); 
                continue;
            }

            var macd = TA.MACD(records);

            var dif = macd[0];
            var dea = macd[1];
            var macd_value = macd[2];

            var isBearishDivergence = records[records.length-2].Close > records[records.length-3].Close 
                                        && macd_value[records.length-2] < 0 &&  macd_value[records.length-3] > 0; //价格升高,但是macd由正转负

            var isBullishDivergence = records[records.length-2].Close < records[records.length-3].Close 
                                        && macd_value[records.length-2] > 0 &&  macd_value[records.length-3] < 0; //价格降低,但是macd由负转正
            
            if (mp == 0 && isBullishDivergence) { //空仓,底背离开多
                p.OpenLong(symbol, 1);
                mp = 1;
            }
            if (mp == 0 && isBearishDivergence) { //空仓,顶背离开空
                p.OpenShort(symbol, 1);
                mp = -1;
            }
            if (mp == 1 && isBearishDivergence) { //持多,顶背离平多
                p.Cover(symbol);
                mp = 0;
            }
            if (mp == -1 && isBullishDivergence) { //持空,顶背离平空
                p.Cover(symbol);
                mp = 0;
            }

            for (var i = 0; i < records.length; i++) {
                var bar = records[i];
                c.begin(bar);
                c.plot(dif[i], 'DIF', {overlay: false});
                c.plot(dea[i], 'DEA', {overlay: false});
                c.plot(macd_value[i], title='MACD', {style : 'histogram',  color: macd_value[i] > 0 ? 'red' : 'green', overlay : false})
                c.plotchar(isBullishDivergence, {char: '底背离', size: "10px", overlay: true});
                c.plotchar(isBearishDivergence, {char: '顶背离', size: "10px", overlay: true});
                c.close();
            }

            Sleep(1000)        
        } else {
            LogStatus(_D(), "未连接CTP!");

            Sleep(1000)
        }
    }
}

在设置好合约,获取k线数据后,首先计算macd的三个值,可以直接使用内置函数,macd是一个两维数组,需要通过两个索引进行获取。接着我们来定义顶背离和底背离。

当收盘价高于前一根K线的收盘价,而MACD值由前一个正值转为负值,我们定义为isBearishDivergence,顶背离。

而当收盘价低于前一根K线的收盘价,但是MACD值由前一个负值转为正值,我们定义为isBullishDivergence,底背离。

如果变量isBearishDivergence的值为true,顶背离出现,可以根据这个信号和仓位,执行开空平多的交易策略。

与之相反的当我们定义为isBullishDivergence,底背离出现的时候,根据仓位,执行开多或者平空。

最后我们使用klinechart函数将macd的三个值和顶背离和底背离的信号在图表中进行呈现。

在图表中,我们对某些背离反转点进行了有效的判断,比如这里的底背离和顶背离的判断,但是整体来看,判断在有些时候是存在一些偏差的,而且该信号但并不能捕捉到所有的市场反转趋势。这说明了背离的出现并不意味着即将发生市场转折,但它可以作为一个警示信号,引起投资者的关注。因此,在使用K线形态进行市场走势分析时,结合技术指标的背离现象可以提供额外的参考,帮助投资者更全面地了解市场的可能走势。然而,投资决策需要综合考虑其他因素,不能仅仅依靠K线形态和背离现象进行判断。

最后我们总结下,K线形态分析作为技术分析的一部分,仍有一些缺点和需要注意的地方。

噪音和虚假信号:市场中存在许多噪音和虚假信号,这可能导致K线形态的错误判断。建议不仅仅依赖于K线形态,还要结合其他技术指标和市场背景进行综合分析。

过度解读:有时候人们可能过度解读某些特定的K线形态,给予其过高的重要性。然而,单单依靠K线形态本身并不能提供完整的市场分析。建议将K线形态作为辅助工具,与其他技术指标、趋势线等进行结合分析。

适用性限制:不同的K线形态在不同的市场和时间周期中的有效性可能会有所不同。某些形态可能在某些市场中表现出良好的预测能力,而在其他市场中则可能不那么有效。建议根据具体的市场和时间周期评估K线形态的适用性。

风险控制:K线形态分析应结合良好的风险控制措施。单纯依靠K线形态进行决策存在风险,因为市场有时候会出现意外的波动或突破。建议结合止损、止盈等风险管理工具,以保护投资和交易资本。

最后,啰嗦几句,K线形态分析只是技术分析的一种工具,网上的很多大v使用它断言一些有关市场趋势和反转信号。然而,它并非完美的预测工具,我们要对这个工具建立正确的认识,在使用的时候需要谨慎,并结合其他技术指标和市场信息进行综合分析和决策。

26:回测评价系统中的指标计算

怎样是一个好的量化策略呢?是收益率秒杀吗。其实一个好的量化策略不仅仅通过收益率来定义。一个高风险高收益的量化策略,在一个单边的行情中可能很吃香,但是遇到极端行情,可能会丢失全部的收益甚至出现极端的亏损。因此,一个好的策略应该在多个市场环境下保持一致的表现,并具备一定的稳定性。策略应该在不同周期和市场条件下表现良好,而不仅仅依赖于某个特定的行情。

因此,在我们制定好量化策略以后,我们可以使用多种指标来评估交易策略的性能。以下是一些常用的指标及其计算方法:

总收益率(Total Returns):表示交易策略的累计收益率。计算方法为总资产的变化百分比:(总资产 - 初始资产) / 初始资产。

年化收益率(Annualized Returns):表示交易策略在一年内的平均收益率。计算方法为总收益率除以回测期间的年交易天数,再乘以252(假设一年有252个交易日)。

夏普比率(Sharpe Ratio):衡量每单位风险所获得的超额收益。计算方法为年化收益率与无风险利率之差的比值除以策略收益率的标准差。

波动率(Volatility):反映交易策略收益的波动程度,衡量风险大小。常用的计算方法有历史波动率和收益率标准差。

最大回撤(Max Drawdown):衡量交易策略从最高点到最低点的最大损失。计算方法为从某个高点到后续低点期间资产净值的最大降幅百分比。

胜率(Winning Rate):表示交易策略盈利交易的比例。计算方法为盈利交易次数除以总交易次数。

这些指标可以帮助我们评估交易策略的风险和收益表现。在回测评价系统中,我们可以通过计算这些指标来得出对策略的综合评价,并根据评价结果做出相应的调整和优化。

这些指标算法在回测系统中都是现成的,当你跑完策略,这些指标会自动呈现。但是由于这些指标的算法是自动集成的,对于我们来说相当于一个黑箱,当我们无法了解指标算法的具体实现细节时,可能会出现以下几个问题:

误解指标含义:缺乏对指标算法的具体了解可能导致我们对指标含义的误解。我们可能无法准确理解指标所衡量的是什么,以及该如何正确解释和解读指标的结果。这可能影响我们对策略绩效的评估和决策。

不适用于特定情境:不同的策略和市场环境可能对指标有不同的要求。如果我们无法了解指标算法的具体实现细节,就无法判断指标是否适用于我们所关注的特定情境。这可能导致我们在评估策略时使用了不恰当或不准确的指标。

无法验证指标准确性:由于无法了解指标算法的实现细节,我们无法对其准确性进行验证。虽然这些指标通常是由专业人士和学术界制定并经过验证的,但我们无法直接验证其背后的具体计算方法。

本节内容我们就以YOUQUANT的回测系统中的回测绩效为例,剖析策略回测中的夏普率、最大回撤、收益率等指标算法,进而可以帮助你搭建自己的回测指标评价系统。

function returnAnalyze(totalAssets, profits, ts, te, period, yearDays) {
    // force by days
    period = 86400000

    if (profits.length == 0) {
        return null
    }
    
    
    var totalReturns = profits[profits.length - 1][1] / totalAssets

    var yearRange = yearDays * 86400000
    var annualizedReturns = (totalReturns * yearRange) / (te - ts)

    // MaxDrawDown
    var maxDrawdown = 0     // 最大回撤
    var maxDrawdownTime = 0 // 最大回撤时间戳
    var maxAssetsTime = 0   // 最大资产时间戳
    var maxDrawdownStartTime = 0 // 最大回撤开始时间
    var winningRate = 0     // 胜率
    var winningResult = 0 
    var maxAssets = totalAssets
    for (var i = 0; i < profits.length; i++) {
        if (i == 0) {
            if (profits[i][1] > 0) {
                winningResult++
            }
        } else {
            if (profits[i][1] > profits[i - 1][1]) {
                winningResult++
            }
        }
        if ((profits[i][1] + totalAssets) > maxAssets) {
            maxAssets = profits[i][1] + totalAssets
            maxAssetsTime = profits[i][0]
        }
        if (maxAssets > 0) {
            var drawDown = 1 - (profits[i][1] + totalAssets) / maxAssets
            if (drawDown > maxDrawdown) {
                maxDrawdown = drawDown
                maxDrawdownTime = profits[i][0]
                maxDrawdownStartTime = maxAssetsTime
            }
        }
    }
    if (profits.length > 0) {
        winningRate = winningResult / profits.length
    }

    // trim profits
    var i = 0
    var datas = []
    var sum = 0
    var preProfit = 0
    var perRatio = 0
    var rangeEnd = te
    if ((te - ts) % period > 0) {
        rangeEnd = (parseInt(te / period) + 1) * period
    }
    for (var n = ts; n < rangeEnd; n += period) {
        var dayProfit = 0.0
        var cut = n + period
        while (i < profits.length && profits[i][0] < cut) {
            dayProfit += (profits[i][1] - preProfit)
            preProfit = profits[i][1]
            i++
        }
        perRatio = ((dayProfit / totalAssets) * yearRange) / period
        sum += perRatio
        datas.push(perRatio)
    }

    var freeProfit = 0.03 // 无风险收益率

    var sharpeRatio = 0
    var volatility = 0
    if (datas.length > 0) {
        var avg = sum / datas.length;
        var std = 0;
        for (i = 0; i < datas.length; i++) {
            std += Math.pow(datas[i] - avg, 2);
        }
        volatility = Math.sqrt(std / datas.length);
        if (volatility !== 0) {
            sharpeRatio = (annualizedReturns - freeProfit) / volatility
        }
    }

    return {
        totalAssets: totalAssets,
        yearDays: yearDays,
        totalReturns: totalReturns,
        annualizedReturns: annualizedReturns,
        sharpeRatio: sharpeRatio,
        volatility: volatility,
        maxDrawdown: maxDrawdown,
        maxDrawdownTime: maxDrawdownTime,
        maxAssetsTime: maxAssetsTime,
        maxDrawdownStartTime: maxDrawdownStartTime,
        winningRate: winningRate
    }
}

我们直接上源码,这个计算指标的函数名定义为returnAnalyze,可以首先来看这个计算函数的输入:

totalAssets
这个参数是策略开始运行时的初始资产总计。

profits
这个参数是一个比较重要的参数,因为一系列的绩效指标计算都是围绕这个原始数据来进行的。这个参数是一个二维数组,每个元素是按照时间戳排列的,包含时间戳和对应的profit,用来记录着各个时刻收益的时间顺序的数据结构。

ts
是回测的开始时间戳。

te
回测的结束时间戳。

period
毫秒级别的计算周期。

yearDays
是一年的交易日,通常情况下定为252天:

接下来我们来看下这些变量是怎样获取以及定义的:

在策略开始的时候,totalAssets,period和yearDays可以定义为常量;

ts可以通过在策略开始的时候定义为常量,而te作为实时更新的变量,可以随着策略的运行进行更新。

这里的重点是profits数组的计算,因为原始的GetAccount和GetPosition都不能直接获取策略在运行期间的总体收益,因此需要我们进行一些处理。

GetAccount的数据结构是这样的,但是这里的Balance需要在一笔交易完成之后,才能获得更新;因此直接使用Balance和起始金额的差值进行收益的计算是不准确的,不能统计在持仓期间内的浮动盈亏的变化;

{
    Info            : {...},     // 请求交易所接口后,交易所接口应答的原始数据,回测时无此属性
    Balance         : 1000,      // 可用余额
    FrozenBalance   : 0,         // 挂单冻结的余额
    Stocks          : 1,         // 传统期货、股票证券此属性固定为0
    FrozenStocks    : 0          // 传统期货、股票证券此属性固定为0
}

而GetPosition统计了每笔交易的收益状况,作为一个数组,它可能包含很多笔交易,而每笔交易包含的收益和保证金是我们统计实时收益所需要的属性。

{
    Info            : {...},     // 请求交易所接口后,交易所接口应答的原始数据,回测时无此属性
    MarginLevel     : 10,        // 杆杠大小,商品期货无法修改杠杆值
    Amount          : 100,       // 持仓量
    FrozenAmount    : 0,         // 挂单平仓时,仓位冻结量
    Price           : 10000,     // 持仓均价
    Profit          : 0,         // 盯市盈亏
    Type            : 0,
    ContractType    : "rb2201",  // 合约代码、股票代码
    Margin          : 1          // 仓位占用的保证金
}

而GetPosition统计了每笔交易的收益状况,作为一个数组,它可能包含很多笔交易,而每笔交易包含的收益和保证金是我们统计实时收益所需要的属性。

因此,总体收益可以通过可用余额加上仓位的浮盈,保证金也需要占据金额,所以一并加上,最后减去初始的金额,就是实时的收益。

Balance + pos_profit + pos_margin - totalAssets

对于多品种策略来说,position可能包含很多的仓位,我们定义了一个函数calculateTotalProfit,使用遍历,将所有的浮盈和保证金相加。

这里需要注意的是position可能包含0个元素,所以这时候不能获取属性,需要使


更多内容