利益を最大化するためにトレーリングストップを実装します。
ちなみにトレーリングエントリーというのは勝手に作った言葉です。
エントリーするときも決済と同じようにトレーリングして、なるべく安く買うorなるべく高く売るというエントリー方法にしたいと考えてます。実際の用語は知らない。あるんだろうか?
まぁ、トレーリングエントリーの実装は後回しかもです。
ただ、こちらも少し頭の隅に置いて実装していきます。
バージョン表記の変更
ちょっと横道にそれますが、バージョンの表記もx.y.zに変更しようと思います。
今のバージョンは1.01。最初のバージョンが1.0で、クレジットを証拠金として使うようにアップデートしてマイナーバージョンを上げてる。
x.y.zで管理するのは一般的なバージョン管理方法で、セマンティック バージョニング というらしい。
APIを定義しないと使えないらしいのだが、EAの場合は設定項目が外部とのインターフェースなので、これをAPIとして、変更する場合はメジャーバージョンを上げることにしようと思います。
主な変更内容
具体的には今回以下の変更を加えます。
設定項目が変わるので、さっそくメジャーバージョンアップということになります。
- 設定項目の追加。トレーリングストップ/エントリーのON/OFFとトレーリング幅を追加。
- 決済判断処理の変更:JudgeClose()関数
- エントリー判断処理の変更:JudgeEntry()関数
トレーリングストップの実装
決済処理を考える
いままでは、決済判断するポジション範囲の計算に許容価格差を考慮してました。
でも、トレーリングエントリーを行うと許容価格差を超えてエントリーすることになります。
なので、売りの決済なら「Ask+利食い幅」よりターゲット価格が上のトラップはすべてチェックするという風に変更します。
上の図の場合、赤いラインより高いターゲット価格の[2]まではすべてチェック。[3]以降はチェックしないことにします。
これは実際のエントリー価格に関わらず「利食いはターゲット価格から利食い幅以上で決済する」のと同じ意味を持ちます。利確数より利幅を優先する感じですね。
次に、決済判断を考えます。
いままでは、判断が2つだけでした。
- ポジションを持っているか?→Noなら次へ
- 購入価格とAskの差が利食い幅以上か?→Yesなら決済する。
今回は、トレーリングストップなので、利食い幅以上利益が出てもすぐには決済しません。
今、仮に以下のような状況を考えます。
⓪のところで売りポジションをエントリー(購入価格のターゲットは79.0円)
利食い幅0.5円、トレーリングストップの幅を0.4円と設定し、0.1円以上またいだら決済価格を更新することにして、現在のAsk価格が①78.4円だとします。
まず、利食い幅を0.1円以上またいだので、決済価格を78.5円に設定します。(※決済価格の更新判定True)
この状態で、②や③に行った場合には決済するかどうかの判定が必要です。(※決済判定True)
なので、利食い幅と決済価格との差をどこまで許容するかが必要になります。
(あるいは、一度決済価格を設定したら価格差が出たとしても決済すると決めるか。)
この②や③の場合、ターゲット価格とAskの価格差は利食い幅を下回ることになるので、確認するトラップのチェックはAsk<ターゲット価格のトラップ全部としないと漏れますね。
また、⓪でエントリーした状態から②や③に行くことがありますが、決済価格の初期設定を0にしてやって、その場合は決済しないという判定をいれるようにします。(※決済判定False)
次に、①から④に行った場合。この時は何もしません。(※決済価格の更新判定False)
なぜなら、「トレーリング幅を0.1円以上またぐ」という判定を満たさないから。
⑤に行った場合は、決済価格を更新します。一気に⑥へ行った場合も決済価格を更新します。
(※いちどに複数のトレーリング幅をまたぐ場合の決算価格更新)
この辺は、whileループを使うか、最初にAskとターゲット価格の差を見てどこを決済価格に設定するか見るようにします。
ということで、だいたい分かりました。整理すると以下の流れで実装してやれば良さそう。
- ポジションを持っているか?→Noなら次へ(変わらず)
- 決済価格の更新を判定(複数ラインまたぎを考慮する)→Trueなら価格更新して次へ、Falseなら決済判定へ
- 決済価格の設定があるか?→Falseなら次へ、Trueなら決済判定へ
- 決済判定(売りポジならAsk≧決済価格、買いポジならBid≦決済価格)→Trueなら決済処理へ
決済価格はポジション情報構造体struct_PositionInfoに追加して保持するようにします。
んじゃ、コーディングいってみましょう。
設定項目の追加
まず、インプットする設定項目を追加。
決済時に許容する目標と実際の価格差は今回は設定しないことにします。
// インプットパラメータ
input double _InputRangeMAX = 101.261; // トラップレンジ上限[円]
input double _InputRangeMIN = 61.669; // トラップレンジ下限[円]
input double _InputProfitMargin = 0.64; // 利食い幅[円]
input int _InputNumSellTraps = 31; // 売りトラップ本数[本]
input int _InputNumBuyTraps = 31; // 買いトラップ本数[本]
input double _InputSellStop = 109.86; // 売りトラップのロスカットライン[円]
input double _InputBuyStop = 53.07; // 買いトラップのロスカットライン[円]
input double _InputDifferenceRimit = 1.0; // 許容する目標価格と約定価格の価格差[円]
input bool _InputTrailingStop = true; // トレーリングストップ設定[on/off]
input double _InputTrailingStopRange= 0.2; // トレーリングストップ幅[円]
input int _InputTrailingStopMargin = 50; // トレーリングストップ幅をどれだけ超えたらストップ価格を更新するか?[%]
input bool _InputTrailingEntry = true; // トレーリングエントリー設定[on/off]
input double _InputTrailingEntryRange = 0.2; // トレーリングエントリー幅[円]
input int _InputTrailingEntryMargin = 50; // トレーリングエントリー幅をどれだけ越えたらエントリー価格を更新するか?[%]
sinput int _InputBarancePercentage = 100; // 残高のうちEAで利用する割合[%]
sinput int _InputSlip = 50; // 許容スリッページ[単位:0.1pips]
ポジション情報に決済価格を追加
決済価格の項目をポジション情報構造体に追加します。
struct struct_PositionInfo{ // 保有ポジション情報構造体
double target_price; // 目標エントリー価格
double entry_price; // エントリー価格
double lots; // オーダーロット数
int ticket_no; // チケットナンバー
double close_price; // 決済価格
};
初期化時にこの決済価格を0に設定します。
以下、InitPosition()関数内で処理追加。
// 買いポジション目標価格とロット数設定
for(icount=0; icount<_InputNumBuyTraps; icount++){
_PositionBuyBuffer[icount].target_price = NormalizeDouble(_InputRangeMIN + HalfBuyTrapWidth*2*icount, _Digits);
_PositionBuyBuffer[icount].lots = _TrapInfoData.BuyLot;
_PositionBuyBuffer[icount].close_price = 0.0;
}
// 売りポジション目標価格とロット数設定
for(icount=0; icount<_InputNumSellTraps; icount++){
_PositionSellBuffer[icount].target_price = NormalizeDouble(_InputRangeMAX - HalfSellTrapWidth*2*icount, _Digits);
_PositionSellBuffer[icount].lots = _TrapInfoData.SellLot;
_PositionSellBuffer[icount].close_price = 0.0;
}
決算判定の処理を修正
決済判定をしている関数はJudgeClose()関数。
トレーリングストップをする場合としない場合で場合分けが必要。
チェック範囲は判定を甘くして、売りはAskより上、買いはBidより下のトラップをすべてチェックするようにします。
// 売りポジションのチェック範囲計算
SellMaxTrapNo = (int)((_InputRangeMAX - Ask)/_TrapInfoData.SellTrapWidth);
if(SellMaxTrapNo >= _InputNumSellTraps){ // レンジのセンターを超えて価格が下がっているとき
SellMaxTrapNo = _InputNumSellTraps-1; // 全部のトラップを調べるようにTrapNoを最大値に設定
}
else if(SellMaxTrapNo < 0){ // レンジ上限より価格が上にいる場合
return; // 損切にしかならないので決済せずに終了
}
// 買いポジションのチェック範囲計算
BuyMaxTrapNo = (int)((Bid-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);
if(BuyMaxTrapNo >= _InputNumBuyTraps) { // レンジのセンターを超えて価格が上昇しているとき
BuyMaxTrapNo = _InputNumBuyTraps-1; // 全部のトラップを調べるようにTrapNoを最大値に設定
}
else if(BuyMaxTrapNo < 0){ // レンジ下限より価格が下にいる場合
return; // 損切にしかならないので決済せず終了
}
あとは、ひたすらさっき考えた処理を実装してく。
JudgeClose()関数内。売り部分だけ載せときます。(買いは逆のことをやってるだけなので)
// 売りポジションの決済チェック
// トレーリングストップ有の場合
if(_InputTrailingStop){
for(icount=0; icount<=SellMaxTrapNo; icount++){
if(_PositionSellBuffer[icount].ticket_no == 0){ // ポジションがオープンしてないなら次へ
continue;
}
// 決済価格更新判定
tmpClosePrice = 0.0; // 決済価格(仮置き)
if((_PositionSellBuffer[icount].target_price-Ask)>(_InputProfitMargin*(1+_InputTrailingStopMargin/100))){ // 利食い幅+決済価格更新マージンを越えた場合のみ価格更新する
int lineNo = (int)(((_PositionSellBuffer[icount].target_price-Ask)-_InputProfitMargin)/_InputTrailingStopRange);
if(lineNo>0){
// lineNo分のトレーリングストップラインを越えて、さらに価格更新マージンを越える場合は価格更新
if( (_PositionSellBuffer[icount].target_price-Ask)>(_InputProfitMargin+_InputTrailingStopRange*lineNo+_InputTrailingStopRange*(_InputTrailingStopMargin/100)) ){
tmpClosePrice = _PositionSellBuffer[icount].target_price-_InputProfitMargin-_InputTrailingStopRange*lineNo;
}else{ // 更新マージンを越えられなかった場合は一つ前のラインで更新。
tmpClosePrice = _PositionSellBuffer[icount].target_price-_InputProfitMargin-_InputTrailingStopRange*(lineNo-1);
}
}
else{ // lineNoが0(トレーリングラインはまたいでない)の場合
tmpClosePrice = _PositionSellBuffer[icount].target_price-_InputProfitMargin; // ターゲット価格―利食い価格を決済価格に設定
}
// 元々の決済価格より決済価格が安い場合と、元が0の時は決済価格を更新。価格更新時は決済しないので次へ
if(_PositionSellBuffer[icount].close_price == 0.0 || _PositionSellBuffer[icount].close_price > tmpClosePrice){
_PositionSellBuffer[icount].close_price = tmpClosePrice;
continue;
}
}
// 決済価格設定が無いなら次へ。(あれば決済判定へ)
if(_PositionSellBuffer[icount].close_price == 0){
continue;
}
// 決済判定
if(_PositionSellBuffer[icount].close_price <= Ask){ // Askが決済価格を越えたら決済
// 決済処理へ
EA_CloseOrder(_PositionSellBuffer[icount].ticket_no);
}
}
}
一通りできたのでバックテスト実施してみる。
バックテストしてみた
ん?なんかおかしい。全然伸びないし負けトレードが増えてる。
と思ったら、一度決済したポジションの決済価格がクリアされてなくて、次に同じトラップで買った時にすぐに決済してるっぽい。(上図の反転してるとこ)
バグ修正
ポジション決済時の処理を修正します。
決済時に決済価格をクリアする処理を追加。
EA_CloseOrder()関数内
else{
// 成功した時の処理。バッファの中身をクリアする。
string sep_str[];
int sep_num;
sep_num = StringSplit(get_comment, ',', sep_str); // TrapNoは4番目
int TrapNo = (int)StringToInteger(sep_str[3]);
if(long_bool){ // 買いポジ
_PositionBuyBuffer[TrapNo].entry_price = 0.0;
_PositionBuyBuffer[TrapNo].ticket_no = 0;
_PositionBuyBuffer[TrapNo].close_price = 0.0;
}
else{ // 売りポジ
_PositionSellBuffer[TrapNo].entry_price = 0.0;
_PositionSellBuffer[TrapNo].ticket_no = 0;
_PositionSellBuffer[TrapNo].close_price = 0.0;
}
ret = true;
}
よーしもう一回テスト。
こんどは正常に動作してる様子。
無しの時と比べてそれほどは利益増えない。。。
無しの純益が1420756.91だったので、年利で0.5%増ぐらいか。
トレーリング幅やまたぎ判定のパーセンテージいじればもう少し良くなるかな?
勢いに乗ってこのままトレーリングエントリーも実装してしまおう。
トレーリングエントリーの実装
トレーリングの方法を考える
トレーリングストップと同様にエントリー目標価格を更新していって、少し戻ったところでエントリーするようにすればいいはず。
利食いが無いのでちょっと楽かな。
エントリー目標価格をポジション情報に追加
ポジション情報用構造体にエントリー目標価格を追加。
トラップの目標価格とか実際のエントリー価格とかあってややこしい。
struct struct_PositionInfo{ // 保有ポジション情報構造体
double target_price; // トラップの目標価格
double entry_price; // エントリー価格
double lots; // オーダーロット数
int ticket_no; // チケットナンバー
double close_price; // 決済目標価格
double entry_target; // エントリー目標価格
};
初期化処理と決済時の処理
さっき痛い目を見たので最初に決済時のクリア処理も追加してしまいます。
初期化処理
InitPosition()関数内
エントリー価格は初期値を0にします。
// 買いポジション目標価格とロット数設定。決済価格は0で初期化
for(icount=0; icount<_InputNumBuyTraps; icount++){
_PositionBuyBuffer[icount].target_price = NormalizeDouble(_InputRangeMIN + HalfBuyTrapWidth*2*icount, _Digits);
_PositionBuyBuffer[icount].lots = _TrapInfoData.BuyLot;
_PositionBuyBuffer[icount].close_price = 0.0;
_PositionBuyBuffer[icount].entry_target = 0.0;
}
// 売りポジション目標価格とロット数設定。決済価格は0で初期化
for(icount=0; icount<_InputNumSellTraps; icount++){
_PositionSellBuffer[icount].target_price = NormalizeDouble(_InputRangeMAX - HalfSellTrapWidth*2*icount, _Digits);
_PositionSellBuffer[icount].lots = _TrapInfoData.SellLot;
_PositionSellBuffer[icount].close_price = 0.0;
_PositionSellBuffer[icount].entry_target = 0.0;
}
決済時処理
EA_CloseOrder()関数内
if(long_bool){ // 買いポジ
_PositionBuyBuffer[TrapNo].entry_price = 0.0;
_PositionBuyBuffer[TrapNo].ticket_no = 0;
_PositionBuyBuffer[TrapNo].close_price = 0.0;
_PositionBuyBuffer[TrapNo].entry_target = 0.0;
}
else{ // 売りポジ
_PositionSellBuffer[TrapNo].entry_price = 0.0;
_PositionSellBuffer[TrapNo].ticket_no = 0;
_PositionSellBuffer[TrapNo].close_price = 0.0;
_PositionSellBuffer[TrapNo].entry_target = 0.0;
}
エントリー判定処理
とりあえずでけた。
解説書くの大変すぎるので後で書きます。。
//+------------------------------------------------------------------+
//| エントリー判定処理
//+------------------------------------------------------------------+
void JudgeEntry()
{
double HighAsk = 0.0;
double LowAsk = 0.0;
double HighBid = 0.0;
double LowBid = 0.0;
double tmpEntryTarget = 0.0;
if(_LastAsk > Ask){
HighAsk = _LastAsk;
LowAsk = Ask;
}else{
HighAsk = Ask;
LowAsk = _LastAsk;
}
if(_LastBid > Bid){
HighBid = _LastBid;
LowBid = Bid;
}else{
HighBid = Bid;
LowBid = _LastBid;
}
_LastAsk = Ask;
_LastBid = Bid;
// 買いエントリー処理
// トレーリングエントリー有り
if(_InputTrailingEntry){
// 買い発生の条件:LowAskがCTRより下
if(LowAsk < _TrapInfoData.CenterPrice)
{
// 最小となるトラップNoを求める。
int minNo = (int)((Ask-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);
if(minNo < 0){
minNo=0;
}
for(int icount=_InputNumBuyTraps-1;icount>=minNo;icount--){
if(_PositionBuyBuffer[icount].ticket_no != 0){ // もしすでにポジションもってたら次へ
continue;
}
// エントリー価格の更新判定
tmpEntryTarget = 0.0; // エントリー価格仮置き
if(_PositionBuyBuffer[icount].target_price-Ask > _InputTrailingEntryRange*_InputTrailingEntryMargin/100){ // AskとTarget価格の差がトレーリング価格のマージンを越えた時だけ更新
int lineNo = (int)((_PositionBuyBuffer[icount].target_price-Ask-_InputTrailingEntryRange*_InputTrailingEntryMargin/100)/_InputTrailingEntryRange);
tmpEntryTarget = _PositionBuyBuffer[icount].target_price - lineNo*_InputTrailingEntryRange;
if(_PositionBuyBuffer[icount].entry_target == 0 || _PositionBuyBuffer[icount].entry_target>tmpEntryTarget){
_PositionBuyBuffer[icount].entry_target = tmpEntryTarget;
}
}
// エントリー価格設定が無ければ次へ
if(_PositionBuyBuffer[icount].entry_target == 0){
continue;
}
// エントリー判定
if(_PositionBuyBuffer[icount].entry_target < Ask){ // Askが設定されてるエントリー目標価格を上回ったら発注
//もし価格が不利な方向に乖離してたらエントリーせず次へ
if(Ask - _PositionBuyBuffer[icount].target_price > _InputDifferenceRimit){
continue;
}
// エントリー
bool long_bool = true;
string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumBuyTraps,icount);
EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
}
}
}
}
// トレーリングエントリー無し
else{
// 買い発生の必要条件:LowAskがCTRより下、またはHighAskがレンジ下限より上
if(LowAsk < _TrapInfoData.CenterPrice &&
HighAsk > _InputRangeMIN)
{
// forループを最小限にするためトラップNoにあたりをつける
int minNo = (int)((LowAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth); // LowAskの価格はminNoのトラップの上にいる
int maxNo = (int)((HighAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth); // HighAskの価格はmaxNoのトラップの上にいる
if(minNo <-1 || maxNo >= _InputNumBuyTraps){
return;
}
for(int icount = minNo+1; icount<=maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
if(_PositionBuyBuffer[icount].ticket_no != 0){ // もし既にポジション持ってたら次へ
continue;
}
if(fabs(Ask-_PositionBuyBuffer[icount].target_price)>_InputDifferenceRimit){ // 目標価格との乖離が大きすぎるので次へ
continue;
}
bool long_bool = true;
string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumBuyTraps,icount);
EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
}
}
}
// 売りエントリー処理
// トレーリングエントリー有り
if(_InputTrailingEntry){
// 売り発生の必要条件:HighBidがCTRより上
if(HighBid > _TrapInfoData.CenterPrice)
{
// 最小となるトラップNoを求める。
int minNo = (int)((_InputRangeMAX-Bid)/_TrapInfoData.SellTrapWidth);
if(minNo < 0){
minNo=0;
}
for(int icount=_InputNumSellTraps-1;icount>=minNo;icount--){
if(_PositionSellBuffer[icount].ticket_no != 0){ // もしすでにポジションもってたら次へ
continue;
}
// エントリー価格の更新判定
tmpEntryTarget = 0.0; // エントリー価格仮置き
if(Bid-_PositionSellBuffer[icount].target_price > _InputTrailingEntryRange*_InputTrailingEntryMargin/100){ // BidとTarget価格の差がトレーリング価格のマージンを越えた時だけ更新
int lineNo = (int)((Bid-_PositionSellBuffer[icount].target_price-_InputTrailingEntryRange*_InputTrailingEntryMargin/100)/_InputTrailingEntryRange);
tmpEntryTarget = _PositionSellBuffer[icount].target_price + lineNo*_InputTrailingEntryRange;
if(_PositionSellBuffer[icount].entry_target == 0 || _PositionSellBuffer[icount].entry_target<tmpEntryTarget){
_PositionSellBuffer[icount].entry_target = tmpEntryTarget;
}
}
// エントリー価格設定が無ければ次へ
if(_PositionSellBuffer[icount].entry_target == 0){
continue;
}
// エントリー判定
if(_PositionSellBuffer[icount].entry_target > Bid){ // Bidが設定されてるエントリー目標価格を下回ったら発注
//もし価格が不利な方向に乖離してたらエントリーせず次へ
if(_PositionSellBuffer[icount].target_price-Bid > _InputDifferenceRimit){
continue;
}
// エントリー
bool long_bool = false;
string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumSellTraps,icount);
EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
}
}
}
}
// トレーリングエントリー無し
else{
// 売り発生の必要条件:HighBidがCTRより上、かつLowBidがレンジ上限より下
if(HighBid > _TrapInfoData.CenterPrice &&
LowBid < _InputRangeMAX)
{
// forループを最小限にするためトラップNoにあたりをつける
int minNo = (int)((_InputRangeMAX-HighBid)/_TrapInfoData.SellTrapWidth); // HighBidの価格はminNoのトラップの下にいる
int maxNo = (int)((_InputRangeMAX-LowBid)/_TrapInfoData.SellTrapWidth); // LowBidの価格はmaxNoのトラップの下にいる
if(minNo <-1 || maxNo >=_InputNumSellTraps){
return;
}
for(int icount=minNo+1; icount <= maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
if(_PositionSellBuffer[icount].ticket_no != 0){ // もしすでにポジションもってたら次へ
continue;
}
if(fabs(Bid-_PositionSellBuffer[icount].target_price)>_InputDifferenceRimit ){ // 目標価格との乖離が大きすぎるので次へ
continue;
}
bool long_bool = false;
string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumSellTraps,icount);
EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
}
}
}
}
ということで完成~!!!
バージョン 2.0.0 バックテスト結果
色々バグ取りしたりして、調整しました。EAのパラメータもちょっといじりました。
トレーリングの価格更新マージンを50%→5%と小さくしたらいい感じになりました。
トラップの本数も増やしてみたけど結果は芳しくないので元に戻しました。
約定時の許容価格差も1円から0.5円に変更。ほとんど影響ないけど。
ということで、見てください!純益のところ。
年利20%以上を叩き出すようになりました!!!
トレーリングエントリーのおかげで資金の利用率が上がってるのが一番効いてるっぽい。
精度は下がるけど10年のテストもやってみたら、年利20%を平均して出せてる。すげーのできちゃった。
さっそく本日2021年1月5日の夜から実戦投入開始しました。お楽しみに。
コメント