设为首页收藏本站

 找回密码
 注册
查看: 9593|回复: 10
打印 上一主题 下一主题

警惕TB序列函数传参机制中的陷阱 [复制链接]

Rank: 2

精华
0
UID
166427
积分
72
帖子
2
主题
1
阅读权限
30
注册时间
2012-5-14
最后登录
2014-6-11
跳转到指定楼层
1#
发表于 2014-6-4 21:19:20 |只看该作者 |倒序浏览

TradeBlazer中有一个编译警告W0201,它的含义是:“FOR,WHILE,IF,ELSE中包含序列函数,可能存在潜在的逻辑错误,请确认代码无误”。

但对这个解释,很多人并不清楚是什么意思。并且我认为,官方文档在这个问题上的解释存在一点误导性,于是拿出来说一说。

所谓“序列函数”,就是带序列类型参数的函数,也就是在Params字段包含了NumericSeries,或者BoolSeries,或者StringSeries类型的参数的函数。

为什么要避免在FOR,WHILE,IF,ELSE中,调用序列函数呢?

网上的一些资料解释说:序列函数需要每个Bar都被调用,如果有些Bar没有调用序列函数,序列函数中的序列数据则是上一个Bar的值。

我来解释一下这句话的意思:

假设主程序中有如下代码:

// 代码段一

Vars
     NumericSeries numSeries;
     Numeric nVal;

Begin
     numSeries = CurrentBar;

     If (CurrentBar % 3 == 0 )
          nVal = Average(numSeries, 3);
     Else If (CurrentBar % 3 == 1 )
          nVal = Average(numSeries, 3);
     Else
          nVal = Average(numSeries, 3);

          FileAppend(“LogFile”, Text(nVal));
End

这段代码的含义很简单,无论当前Bar的序号对3的余数是0、1、还是2(当然只有这三种情况),都把numSeries的最后三个元素的平均数打印出来。而numSeries 实际上就是CurrentBar的序号序列:0、1、2、3、4……

这段代码在编译中,会出现本文开始时候提到的警告提示,因为我们在If体中引用了序列函数Average,并提示我们“可能存在潜在的逻辑错误”。

可是,这段代码真的有逻辑错误吗?从逻辑上来讲,如果一个条件成立就执行A,否则也执行A,那就是说,无论如何都要执行A,那么这个逻辑和直接执行A就是等价的。所以上述代码等价于如下代码:

// 代码段二


Vars
     NumericSeries numSeries;
     Numeric nVal;

Begin
     numSeries = CurrentBar;
     nVal = Average(numSeries, 3);
     FileAppend(“LogFile”, Text(nVal));
End

OK,这段代码的编译没有警告出现,而且“逻辑上和代码段一是等价的”。但是,如果你真的这样认为,那就上当了。我们先看看代码段二的输出:

0:          0
1:          0.333333
2:          1
3:          2
4:          3
5:          4
6:          5
7:          6
……
……

序号是2及以后的Bar的输出很好理解,因为从CurrentBar开始向前回溯,连续三个数的平均数是 CurrentBar - 1,这一点毫无疑问。问题是最前面的两项。当CurrentBar是0的时候,Average函数的输出是0,而当CurrentBar为1的时候,Average的输出是1/3。也许你会认为,Average很聪明,当CurrentBar是0和1的时候,向前回溯3个元素是无法做到的,所以它就只“尽力”回溯到可以回溯的地方。在CurrentBar为0的时候,它无法向前回溯,所以计算平均值就是 0 / 3 = 0;在CurrentBar为1的时候,它只向前回溯1个元素,所以计算平均值就是(0+1) / 3 = 0.333333

这一切看起来很合理,但如果我们稍微改动一下程序,你马上就发现不是那么回事了。


// 代码段三


Vars
     NumericSeries numSeries;
     Numeric nVal;

Begin
     numSeries = CurrentBar + 1;
     nVal = Average(numSeries, 3);
     FileAppend(“LogFile”, Text(nVal));
End

对代码段三来说,如果按照之前的推理,前两个平均数输出应该是1 / 3,和 (1 + 2)/ 3 = 1,但事实上它的输出是:

0:          1
1:          1.333333
2:          2
3:          3
4:          4
5:          5
6:          6
7:          7
……
……

之所以会出现这种情况,是因为出现了所谓的回溯越界。当CurrentBar为0时,引用numSeries[1]就会引发越界。据说在老版本的TB中,越界会返回一个无效值,但现在的TB会返回最近的一个有效元素的值。对于numSeries[1]来说,TB会返回numSeries(也就是numSeries[0])的值作为结果。同样的,此时对于numSeries[2], numSeries[3], …… numSeries[N]来说,它们的值都是一样的,都是numSeries的值。

所以,对代码三来说,CurrentBar为0时,Average的计算并不是(0+0+1)/3,而是(1+1+1)/3,当CurrentBar为1的时候,Average的计算是 (1+1+2)/ 3,于是出现了上面的结果。

好,之所以说了这么一大堆关于回溯越界的事情,是因为这样可以更好地了解TB的传参机制。现在回到前面,继续之前的分析。如果说,代码段二和代码段一是等价的,那么它们的输出应该完全一致。那事实上是不是这样呢?我们再来看下代码段一的输出。为了方便比较,我们把代码一和二的两组输出放在一起观察:

代码段一的输出:
0:          0
1:          1
2:          2
3:          3
4:          4
5:          5
6:          6
7:          7
……
……

代码段二的输出:

0:          0
1:          0.333333
2:          1
3:          2
4:          3
5:          4
6:          5
7:          6
……
……

看,貌似逻辑相同的两段代码,输出的结果却不同。我认为这是TB设计上的缺陷。但关键是,我们得知道问题是怎么发生的。

我们假设,对代码段一来说,当前的CurrentBar是2,这时候numSeries已经保存了3个元素,从左到右(从旧到新)分别是0、1、2。此时,CurrentBar % 3 的结果是2,所以代码进入了If-Else结构的最后一个分支,开始调用Average(numSeries, 3)来计算三者的平均值。按照我们的设想,Average的计算过程应该是(0 + 1 + 2)/ 3 = 1,可查看代码一的结果,却发现,输出居然是2 !!

其实,问题就出在序列函数的传参机制上。

当程序在If-Else结构的最后一个分支调用Average的时候,代码并没有原原本本地把参数numSeries传给它。C++或者Java的程序员,已经习惯了关于对象的引用传参方式,大多会想当然地认为Average接收到的就是0、1、2序列,可事实上却不是。

出于某种原因,TB除了三种基本类型以外,并没有为序列变量设计引用方式的传参。当我们调用Average(numSeries, 3)的时候,Average函数在作用域内部构造一个NumericSeries内部变量用于接收参数,可是基于效率的考虑,TB自然不会把整个序列变量全盘拷贝,它采取了最直接的方式,把numSeries[0],赋给了Average的内部变量。注意,这是处于If-Else结构的最后分支上的Average函数第一次被调用,前面两个Bar(CurrentBar  = 0 和 CurrentBar  = 1)并没有给这个内部变量赋予任何东西。于是,对于CurrentBar = 2那一刻来说,Average的内部序列变量中保存的值序列是:N/A, N/A, 2

此时,由于前两个值是无效的,对内部序列的回溯产生了越界,于是按照TB的机制,越界回溯返回了最后一个有效元素的值,也就是2,因此在这一刻,Average的实际计算方式是:(2 + 2 + 2)/3 = 2

看到了吧,结果就是这么发生的。

事实上,由于代码段一中的If-Else模块共有三个分支,在每个分支上的Average都有一个自己的内部变量,用于接收参数传递,所以,不管代码在那个分支上执行,Average得到的都不是一个完整的numSeries序列。更要命的是,它们都只按照自己内部的序列变量来计算均值,于是导致了最终的结果。

为什么要花这么大的篇幅来讲这个事情呢?

因为我觉得TB在W0201警告中的说明太含糊了,甚至不完全准确。它只考虑到在FOR,WHILE,IF,ELSE这些判断类条件中,序列变量的传参问题可能会带来序列函数内部变量的不连续性,但事实上情况比这个要复杂。

看下面这个代码:


Vars
     NumericSeries numSeries;
     Numeric nVal;

Begin
     numSeries = 1;
     nVal = Average(numSeries, 3);
     FileAppend(“LogFile”, Text(nVal));
     

     numSeries = 2;
     nVal = Average(numSeries, 3);
     FileAppend(“LogFile”, Text(nVal));

     numSeries = 3;
     nVal = Average(numSeries, 3);
     FileAppend(“LogFile”, Text(nVal));
End  

你认为它的输出会是什么?

在每个Bar里面,numSeries会被重复赋值,最后一次赋值总是3。当然,我们有理由相信,当CurrentBar=2到来时,numSeries里面的元素应该是numSeries[2]=3,numSeries[1]=3,此时,执行第一个求平均,numSeries[0]被赋予1,那么Average计算的应该是(3 + 3 + 1) / 3。可事实上是不是这样呢?来看一下真正的输出吧:

0:          1
0:          2
0:          3
1:          1
1:          2
1:          3
2:          1
2:          2
2:          3
3:          1
3:          2
3:          3
……
……

具体原因参照之前的分析就好了。我们可以看到,即便不是在FOR,WHILE,IF,ELSE这些句块当中,我们在主程序中多次给序列变量赋值,并且用不同的值调用序列函数,也会得到不同的结果。这是由序列函数的传参机制决定的,跟在哪个句块当中出现,没有关系。

当然,听TB的警告,不要在FOR,WHILE,IF,ELSE中包含序列函数是对的,另外,看过本文,你也可以知道如何分析 “可能存在潜在的逻辑错误”了。
已有 9 人评分威望 收起 理由
beckall + 2 很给力!
xuxinqujing + 2 很给力!
xyryan + 2 赞一个!
傻了吧 + 36 很给力!
cjqh660861 + 2 赞一个!

总评分: 威望 + 69   查看全部评分

Rank: 1

精华
0
UID
135730
积分
1
帖子
1
主题
0
阅读权限
10
注册时间
2012-9-21
最后登录
2014-6-6
2#
发表于 2014-6-6 09:08:09 |只看该作者
Thanks!

使用道具 举报

Rank: 1

精华
0
UID
169902
积分
11
帖子
1
主题
0
阅读权限
10
注册时间
2013-8-14
最后登录
2014-6-17
3#
发表于 2014-6-12 22:11:21 |只看该作者
楼主分析的很透彻,赞!

使用道具 举报

Rank: 1

精华
0
UID
180726
积分
4
帖子
3
主题
1
阅读权限
10
注册时间
2014-2-18
最后登录
2014-9-9
4#
发表于 2014-6-14 17:48:05 |只看该作者
分析得很精彩,赞一个。另外我怀疑在452版中,每个新bar的第一个tick来到时,似乎A_buyposition函数会不能正常返回数值,不知楼主是否有研究?

使用道具 举报

Rank: 1

精华
0
UID
157429
积分
13
帖子
1
主题
0
阅读权限
10
注册时间
2013-5-29
最后登录
2022-6-20
5#
发表于 2014-7-11 09:47:58 |只看该作者
本帖最后由 wangjiecifco 于 2014-7-11 09:59 编辑

楼主的论述解决了我在一个策略中遇到的问题.   当时死活没有想明白为什么程序输出结果跟预期不符合. 现在完全整明白了.   特别是你的那句 "C++或者Java的程序员,已经习惯了关于对象的引用传参方式"   让我醍醐灌顶啊.   我想应该是所有熟悉了面向对象编程的人在这里都会掉进这个误区.     TB论坛里难得一见的好帖子.



按照楼主的论述,序列函数内部是复制传递进去的序列参数,不论是一次复制当前BAR所对应的值还是复制当前BAR以及之前的整个序列,这样当然会存在效率问题. 一个序列数值相当于一个数组,把这个数组传到另一个函数,个人觉得把该数组的引用传过去效率最高,不用频繁的操作内存. 据说TB编译是把用户的代码编译成C++语言,然后再编译成机器码而执行的.   那么在这方面的改进应该不会很困难吧.  是时候在TB里面引入些许面向对象编程的思想了.

使用道具 举报

Rank: 1

精华
0
UID
127156
积分
2
帖子
2
主题
0
阅读权限
10
注册时间
2013-5-27
最后登录
2014-7-23
6#
发表于 2014-7-19 09:51:01 |只看该作者
好贴,技术贴

使用道具 举报

Rank: 1

精华
0
UID
212587
积分
6
帖子
4
主题
2
阅读权限
10
注册时间
2015-7-2
最后登录
2015-8-7
7#
发表于 2015-8-7 10:37:44 |只看该作者
和if等语句有关。
比如
Vars
        NumericSeries startnow(0);
Begin
        if(BarStatus==0)
        {
        startnow=1;
        }

       
if((Date != Date[1]) )//判断每天的第一根bar.当时全图第一根bar时,false
{  
        startnow=startnow+1;
}
        Commentary("startnow="+Text(startnow));
        Commentary("Q_HIGH="+Text(Q_HIGH[1]));
        Commentary("Highd[1]="+Text(Highd[1]));
        Commentary("Lowd[1]="+Text(Lowd[1]));
        Commentary("CloseD[2]="+Text(CloseD[2]));
        Return;
       
End

Vars
        NumericSeries startnow(0);
Begin
        if(BarStatus==0)
        {
        startnow=1;
        }

       
if((Date != Date[1]) )//判断每天的第一根bar.当时全图第一根bar时,false
{  
        startnow=startnow+1;

        Commentary("startnow="+Text(startnow));
        Commentary("Q_HIGH="+Text(Q_HIGH[1]));
        Commentary("Highd[1]="+Text(Highd[1]));
        Commentary("Lowd[1]="+Text(Lowd[1]));
        Commentary("CloseD[2]="+Text(CloseD[2]));
        Return;
}       
End
在第二天的输出是不同的。
Begin

                Commentary("Highd[1]="+Text(Highd[1]));
                Commentary("Lowd[1]="+Text(Lowd[1]));
                Commentary("CloseD[2]="+Text(CloseD[2]));
                Commentary("Highd[1]-Lowd[1]="+Text(Highd[1]-Lowd[1]));
                Commentary("HIGHd[1]-CloseD[2]="+Text(HIGHd[1]-CloseD[2]));
                Commentary("CloseD[2]-LOWd[1]="+Text(CloseD[2]-LOWd[1]));
                Commentary("Max(HIGHd[1]-CloseD[2],CloseD[2]-LOWd[1])="+Text(Max(HIGHd[1]-CloseD[2],CloseD[2]-LOWd[1])));

       
        if(BarStatus==0)
        {
        SetGlobalVar(0,0);//第0个变量记录多仓单位数量
        SetGlobalVar(1,0);//第一个变量记录空仓单位量
        SetGlobalVar(2,0);//最近的建多藏价格
        SetGlobalVar(3,0);//最近的建空仓价格
        startnow=1;
        Return;
        }
end

Begin
       
        if(BarStatus==0)
        {
        SetGlobalVar(0,0);//第0个变量记录多仓单位数量
        SetGlobalVar(1,0);//第一个变量记录空仓单位量
        SetGlobalVar(2,0);//最近的建多藏价格
        SetGlobalVar(3,0);//最近的建空仓价格
        startnow=1;
        Return;
        }
                Commentary("Highd[1]="+Text(Highd[1]));
                Commentary("Lowd[1]="+Text(Lowd[1]));
                Commentary("CloseD[2]="+Text(CloseD[2]));
                Commentary("Highd[1]-Lowd[1]="+Text(Highd[1]-Lowd[1]));
                Commentary("HIGHd[1]-CloseD[2]="+Text(HIGHd[1]-CloseD[2]));
                Commentary("CloseD[2]-LOWd[1]="+Text(CloseD[2]-LOWd[1]));
                Commentary("Max(HIGHd[1]-CloseD[2],CloseD[2]-LOWd[1])="+Text(Max(HIGHd[1]-CloseD[2],CloseD[2]-LOWd[1])));


end
在第二天的输出结果也不同、。。。。请问tb的开发人员,这什么bug

使用道具 举报

Rank: 6Rank: 6

精华
0
UID
208212
积分
2006
帖子
103
主题
24
阅读权限
70
注册时间
2015-5-2
最后登录
2019-6-18
8#
发表于 2015-9-24 10:52:27 |只看该作者
分析得很精彩,赞一个。

使用道具 举报

Rank: 6Rank: 6

精华
0
UID
208212
积分
2006
帖子
103
主题
24
阅读权限
70
注册时间
2015-5-2
最后登录
2019-6-18
9#
发表于 2015-9-24 10:53:06 |只看该作者
不知道TB对这个有什么有帮助的解释?

使用道具 举报

Rank: 1

精华
0
UID
230338
积分
27
帖子
26
主题
0
阅读权限
10
注册时间
2016-3-18
最后登录
2017-4-30
10#
发表于 2016-3-29 20:14:34 |只看该作者

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

bottom

静态版|手机版|联系我们|交易开拓者 ( 粤ICP备07044698   

GMT+8, 2024-5-3 22:39

Powered by Discuz! X2 LicensedChrome插件扩展

© 2011-2012 交易开拓者 Inc.

回顶部