🏙️

Gemini 2.5 Proがバージョンアップ コーディング強化(実感あり)

はじめに

日本時間2025年5月7日に、GoogleからGemini 2.5 Proがバージョンアップされたとアナウンスがありました。コーディングが強化されているようです。私自身も使ってみましたが、ソースファイル間の整合性を考えた改修案や、指示だけではなくあるべき姿の提案もあり、かなり良い印象を持ちました!
https://x.com/GoogleDeepMind/status/1919770265711419826
Google AI Studio で利用できます!

Google AI StudioでGemini 2.5 Proの最新をモデル選択している様子

Gemini 2.5 Proを使っている様子

具体的に、改修案を考えてもらっている動画は下記です。
https://youtu.be/18nBk-abFog

他モデルとの比較

上記動画の内容について、「Claude 3.7 Sonnet」「GPT-4.1」「o3」にも考えてもらいました。さらに「o3」と「Gemini 2.5 Pro」の融合案を「Gemini 2.5 Pro」に考えてもらいました。

参考までに、各パターンの特徴を表に整理したので、参考にしてみてください。

観点 Gemini初期案 Claude案 GPT-4.1案 o3案 Gemini最終案
簡潔な特徴 マジックナンバーでEA管理を強化し、既存フラグ(g_PartialCloseSet等)の管理を厳格化。約定・キャンセル・決済時のフラグ更新を重視。 部分決済済みチケットのリストを作成し、多重決済を防止。フラグ依存を減らし、約定後情報でリアルタイム計算。 g_PartialCloseSet等のフラグ管理を維持しつつ、状態リセット関数(ResetPartialCloseState)を導入。各イベントで確実なリセットを目指す。 enumによる状態定義(ステートマシン)を導入し、部分決済のライフサイクルを明確に管理。状態遷移をロジックの中心に据える。 o3案のステートマシンをベースに、マジックナンバー連携、チケット管理の明確化、PC_DONEからの遷移など、より詳細な状態管理と安全性を追求。
影響度(既存コード) 中。グローバル変数と主要な注文管理関数に修正が必要。 中~大。新しいグローバル配列の導入と、部分決済ロジックの全面的な書き換えに近い。 中。リセット関数の追加と、その呼び出し箇所の追加が主。既存ロジックの骨子は維持しやすい。 大。状態管理の根本的な変更。グローバル変数の置き換えと、多くの関数での状態遷移ロジックの追加が必要。 大。o3案と同様、状態管理の根本的変更に加え、関連するグローバル変数や関数の役割を再定義。
可用性(導入しやすさ) やや容易。既存のフラグベースの考え方を拡張するため、理解しやすい。 やや複雑。配列管理(上限、クリア)や、EAの意図との連携に注意が必要。 比較的容易。リセット処理の追加という形で、段階的な導入も可能。 やや複雑。ステートマシンの概念理解と、各状態遷移を正確に実装する必要がある。 複雑。ステートマシンの概念に加え、マジックナンバーやチケット変数との連携を正確に実装する必要があり、考慮事項が多い。
改修難易度 中。フラグの更新漏れがないか慎重な確認が必要。 中。配列の適切な管理(特にクリア処理)が課題。パフォーマンスへの配慮も必要。 中。リセット関数を呼び出すべき箇所を網羅的に特定し、追加する必要がある。 やや高。状態遷移の設計と実装の正確性が求められる。デバッグも状態を意識する必要がある。 高。最も包括的な変更であり、状態遷移、マジックナンバー、チケット管理の全てを正確に連携させる高度な理解と実装スキルが求められる。
機能性(堅牢性) 向上。マジックナンバーによりEA管理が明確化され、フラグ管理厳格化でバグ減。 向上。多重決済は防げるが、EAの意図しないポジションへの適用リスクや配列管理の課題が残る可能性。 向上。リセット処理の徹底により、フラグ残留バグは大幅に減る。 大幅に向上。状態遷移が明確になり、予期せぬ動作を排除しやすい。最も堅牢なアプローチの一つ。 最も向上。ステートマシンに加え、マジックナンバーと厳密なチケット管理により、EAの意図通りの動作と状態の整合性が極めて高くなる。
機能性(拡張性) 限定的。フラグベースのため、複雑な拡張(多段決済など)は難しい。 やや限定的。配列ベースのため、状態に応じた複雑なロジック拡張は設計次第。 限定的。フラグベースの限界は残る。 高い。ステートマシンは新しい状態や遷移を追加することで、多段決済や複雑な注文管理ロジックにも対応しやすい。 非常に高い。ステートマシンを基盤とし、管理対象も明確なため、将来的な機能追加(多段決済、高度なリスク管理など)への適応が容易。
保守性 やや向上。フラグの役割が明確になれば。 やや低下の可能性。配列管理ロジックが複雑化すると、追いにくくなる。 向上。リセット処理が一元化されれば、問題発生時の原因特定がしやすくなる。 大幅に向上。状態と遷移がコード上で明示されるため、ロジックの流れが追いやすく、修正も状態単位で行える。 大幅に向上。状態、マジックナンバー、チケットの役割が明確で、ロジックが構造化されるため、理解しやすく修正も安全に行いやすい。
計算コスト ほぼ変わらず。 増加の可能性。OnTick毎の全ポジションスキャンと配列スキャンは負荷になる場合あり。 ほぼ変わらず。 わずかに増加の可能性(状態判定)。ただし無視できるレベル。 わずかに増加の可能性(状態判定)。ただし無視できるレベル。
コード量 増加(中程度)。 増加(中~大)。 増加(小~中程度)。 増加(大)。enum定義や状態遷移ロジックの追加。 増加(大)。o3案に加え、連携のためのコードが追加される。

総括:

  • Gemini初期案GPT-4.1案 は、既存の枠組みの中で堅実な改善を目指すアプローチです。導入は比較的容易ですが、根本的な状態管理の複雑さは残ります。
  • Claude案 は特定の問題(多重決済)への対策としては有効ですが、管理の複雑さやEAの意図との連携に課題が残る可能性があります。
  • o3案 は、ステートマシンという強力な設計パターンを導入することで、ロジックの明確性と堅牢性を飛躍的に高める提案です。改修の労力は大きいですが、得られるメリットも大きいです。
  • Gemini最終案 は、o3案の優れたステートマシンを基盤としつつ、EA開発で不可欠なマジックナンバー管理や、アクティブな注文/ポジションのチケット管理との連携をより具体的に落とし込み、安全性と網羅性を高めたものです。最も包括的で堅牢なソリューションですが、実装の難易度も最も高くなります。

(参考)実際のアウトプット

今回の検証で得たアウトプットを記載しておきます。参考までに

Gemini初期案 (文字数制限のため一部割愛)

ご指摘の通り、RiskManagement.mqhCheckAndExecutePartialClose 関数における g_PartialCloseSet フラグの扱いは、約定前の段階でリセットされてしまう可能性があり、部分決済が意図通りに機能しない原因となり得ます。また、このフラグの管理はEA全体の注文・ポジションライフサイクルと密接に関連しており、修正は慎重に行う必要があります。

以下に、問題解決とロジック改善のための改修案を段階的に説明します。
主な方針は以下の通りです。

  1. マジックナンバーの導入: EAが発行した注文とポジションを確実に識別するために、マジックナンバーを使用します。これは、EAの動作の堅牢性を大幅に向上させます。
  2. g_TicketLong/g_TicketShort の役割明確化: これらの変数は、EAが現在アクティブに管理している(指値または約定済み)ロング/ショート注文(ポジション)のチケット番号を保持するようにします。
  3. g_PartialCloseSet の管理:
    • 指値注文発注時に部分決済が有効なら true に設定。
    • 部分決済実行後に false にリセット。
    • 関連するポジションが完全に決済された(SL/TP、強制決済、手動決済など)場合にも false にリセット。
    • 関連する指値注文が約定せずにキャンセルされた場合にも false にリセット。
  4. 約定処理の明確化: 指値注文が約定した際に、g_TicketLong/g_TicketShort を新しいポジションのチケット番号に更新し、関連フラグ(g_PendingLongDetectedなど)を適切に処理します。

改修ステップ:

ステップ1: マジックナンバーの導入 (EURUSD_Surge_EA.txt)

EAが発行する注文を一意に識別するため、マジックナンバーを導入します。

// グローバル変数
// ... (既存のグローバル変数) ...
int g_MagicNumber;                            // EA専用のマジックナンバー
bool g_PartialCloseSet = false;               // 部分決済設定フラグ (RiskManagement.mqh から移動、または mq4側で管理するならそのままでも良いが、意味合いを明確化)
double g_PartialCloseTriggerPrice = 0;        // 部分決済トリガー価格 (同上)
int g_PartialCloseDirection = 0;              // 部分決済方向 (同上)

// OnInit() 関数内
int OnInit()
{
    // ... (既存の初期化処理) ...
    
    // マジックナンバーの初期化 (例: 口座番号と現在時刻の組み合わせなど)
    g_MagicNumber = (int)AccountInfoInteger(ACCOUNT_LOGIN) % 100000 + (int)StringToInteger(TimeToString(TimeCurrent(), "mmss"));
    if (g_MagicNumber == 0) g_MagicNumber = 12345; // 0は避ける
    LogInfo("EA初期化完了。マジックナンバー: " + IntegerToString(g_MagicNumber));

    // ...
    return(INIT_SUCCEEDED);
}

// OnTick() 関数内の注文発行部分 (ロングの例)
// ...
ticket = SendOrderWithRetry(OP_BUYLIMIT, lotSize, entryPrice, 3, adjustedStopLoss, takeProfit, 
                            "EURUSD Surge EA Advanced Improved", g_MagicNumber, 0, Green); // magic パラメータに g_MagicNumber を設定
// ... (ショートも同様に修正) ...

ステップ2: RiskManagement.mqhSetupPartialCloseCheckAndExecutePartialClose の修正

g_PartialCloseSet などの変数は、EURUSD_Surge_EA.mq4 でグローバル変数として管理することを推奨します。これにより、EAのメインロジックとリスク管理モジュール間での状態共有が明確になります。もし RiskManagement.mqh 内で extern として宣言し続ける場合は、EA側でこれらの変数を適切に初期化・管理する必要があります。ここでは、EA側で管理する前提で進めます。

// RiskManagement.mqh

// ... (CalculatePositionSize, CalculateDynamicStopLoss, GetATRBasedStopLoss, GetZigZagBasedStopLoss, FindRecentZigZagPoint, CalculateTakeProfit は変更なし) ...

// 部分決済の設定 (EAメインファイルから呼び出される)
void SetupPartialClose(string symbol, int ticket, double entryPrice, double stopLoss, int direction)
{
    if(!UsePartialClose) // EA側のグローバル変数 UsePartialClose を参照
    {
        return;
    }
    
    // チケット番号は EA 側で g_TicketLong / g_TicketShort に保存済みとする
    // ここではトリガー価格と方向を計算し、EA側のグローバル変数に設定する
    
    double riskDistance = MathAbs(entryPrice - stopLoss);
    double triggerDistance = riskDistance * PartialCloseTrigger; // EA側の PartialCloseTrigger
    
    if(direction > 0) // ロング
    {
        g_PartialCloseTriggerPrice = NormalizeDouble(entryPrice + triggerDistance, Digits);
    }
    else // ショート
    {
        g_PartialCloseTriggerPrice = NormalizeDouble(entryPrice - triggerDistance, Digits);
    }
    
    g_PartialCloseDirection = direction;
    g_PartialCloseSet = true; // EA側の g_PartialCloseSet を true に
    
    LogInfo("部分決済設定 (Setup): Ticket=" + IntegerToString(ticket) + // 渡されたチケットは指値注文のチケット
           ", トリガー価格=" + DoubleToString(g_PartialCloseTriggerPrice, Digits) + 
           ", 方向=" + IntegerToString(direction));
}

// 部分決済条件をチェックして実行 (EAメインファイルから呼び出される)
void CheckAndExecutePartialClose(string symbol)
{
    // EA側のグローバル変数 UsePartialClose, g_PartialCloseSet を参照
    if (!UsePartialClose || !g_PartialCloseSet || g_PartialCloseDirection == 0)
    {
        return;
    }

    int ticketToMonitor = 0;
    int expectedOrderType = 0;

    if (g_PartialCloseDirection > 0) // ロングの部分決済設定
    {
        ticketToMonitor = g_TicketLong; // EA側の g_TicketLong (約定済みポジションのチケット)
        expectedOrderType = OP_BUY;
    }
    else if (g_PartialCloseDirection < 0) // ショートの部分決済設定
    {
        ticketToMonitor = g_TicketShort; // EA側の g_TicketShort (約定済みポジションのチケット)
        expectedOrderType = OP_SELL;
    }

    if (ticketToMonitor == 0) // 監視すべきアクティブなポジションがない
    {
        // この場合、部分決済設定はもはや有効ではないのでリセット
        if (g_PartialCloseSet) {
             LogInfo("CheckAndExecutePartialClose: 監視対象チケットなし。部分決済設定をリセット。");
             g_PartialCloseSet = false;
             g_PartialCloseDirection = 0;
             g_PartialCloseTriggerPrice = 0;
        }
        return;
    }

    if (OrderSelect(ticketToMonitor, SELECT_BY_TICKET, MODE_TRADES))
    {
        // EAが発行した約定済みポジションか確認
        if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol && OrderType() == expectedOrderType)
        {
            double currentPrice = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK);
            bool triggerCondition = false;

            if (OrderType() == OP_BUY)
            {
                triggerCondition = (currentPrice >= g_PartialCloseTriggerPrice && g_PartialCloseTriggerPrice != 0);
            }
            else // OP_SELL
            {
                triggerCondition = (currentPrice <= g_PartialCloseTriggerPrice && g_PartialCloseTriggerPrice != 0);
            }

            if (triggerCondition)
            {
                double initialLots = OrderLots();
                double closeVolume = NormalizeDouble(initialLots * (PartialClosePercent / 100.0), 2); // EA側の PartialClosePercent
                double minLot = MarketInfo(OrderSymbol(), MODE_MINLOT);
                double lotStep = MarketInfo(OrderSymbol(), MODE_LOTSTEP);
                
                closeVolume = MathMax(minLot, MathFloor(closeVolume / lotStep) * lotStep);

                if (closeVolume >= minLot && closeVolume < initialLots) // 全決済にならないように
                {
                    bool result = false;
                    double closePrice = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK);
                    
                    if (OrderType() == OP_BUY)
                    {
                        result = OrderClose(ticketToMonitor, closeVolume, closePrice, 3, Green);
                    }
                    else // OP_SELL
                    {
                        result = OrderClose(ticketToMonitor, closeVolume, closePrice, 3, Red);
                    }

                    if (result)
                    {
                        LogInfo("部分決済成功: Ticket=" + IntegerToString(ticketToMonitor) +
                               ", Lots=" + DoubleToString(closeVolume, 2) + "/" + DoubleToString(initialLots, 2) +
                               " at " + DoubleToString(closePrice, Digits));
                        g_PartialCloseSet = false; // 部分決済が完了したらリセット
                        g_PartialCloseDirection = 0;
                        g_PartialCloseTriggerPrice = 0;
                    }
                    else
                    {
                        LogError("部分決済エラー: " + GetLastErrorText(GetLastError()) + " Ticket: " + IntegerToString(ticketToMonitor));
                    }
                }
                else if (closeVolume >= initialLots) {
                     LogInfo("部分決済スキップ: 計算ロット(" + DoubleToString(closeVolume,2) + ")が全ロット(" + DoubleToString(initialLots,2) + ")以上。実質全決済とみなしフラグリセット。");
                     g_PartialCloseSet = false;
                     g_PartialCloseDirection = 0;
                     g_PartialCloseTriggerPrice = 0;
                }
                 else { // closeVolume < minLot
                    LogInfo("部分決済スキップ: 計算ロット(" + DoubleToString(closeVolume,2) + ")が最小ロット(" + DoubleToString(minLot,2) + ")未満。フラグリセット。");
                    g_PartialCloseSet = false;
                    g_PartialCloseDirection = 0;
                    g_PartialCloseTriggerPrice = 0;
                 }
            }
        }
        // OrderSelectは成功したが、MagicNumberやTypeが期待と異なる場合は何もしない (このEAの管理外か、状態不整合)
    }
    else // OrderSelect が false = ポジションが存在しない (決済済み or 存在しないチケット)
    {
        // 監視対象だったポジションが存在しない場合、部分決済設定はリセット
        if (g_PartialCloseSet) // フラグがまだ立っていたら
        {
            int error = GetLastError();
             // ERR_NO_RESULT は「注文が見つからない」で正常なケースもある
            if (error != ERR_NO_RESULT && error != 0) {
                LogWarning("CheckAndExecutePartialClose: OrderSelectエラー Ticket " + IntegerToString(ticketToMonitor) + ": " + GetLastErrorText(error));
            }
            LogInfo("CheckAndExecutePartialClose: Ticket " + IntegerToString(ticketToMonitor) + " が存在しないため部分決済設定をリセット。");
            g_PartialCloseSet = false;
            g_PartialCloseDirection = 0;
            g_PartialCloseTriggerPrice = 0;
        }
    }
}

ステップ3: EURUSD_Surge_EA.txt のポジション・注文管理ロジック修正

OnTick, ManagePendingOrders, CloseAllPositions を修正し、マジックナンバーと部分決済フラグを適切に扱います。

// EURUSD_Surge_EA.txt

// ... (グローバル変数、OnInit はステップ1で修正済み) ...

// OnTick() 関数
void OnTick()
{
    if(IsNewDay())
    {
        ResetDailyCounters();
        LogInfo("新しい日を検出: カウンターをリセットしました");
    }

    // --- ポジション状態監視とフラグ整合性チェック ---
    // g_TicketLong/Short が指すポジションが存在しなくなった場合 (SL/TP/手動決済など)
    // または、指値注文がキャンセルされた後にチケットがクリアされていない場合
    if (g_TicketLong != 0) {
        if (!OrderSelect(g_TicketLong, SELECT_BY_TICKET, MODE_TRADES) || 
            (OrderMagicNumber() != g_MagicNumber || (OrderType() != OP_BUY && OrderType() != OP_BUYLIMIT)) ) {
            LogInfo("管理中のロング注文/ポジション(Ticket:" + IntegerToString(g_TicketLong) + ")が見つからないか、EA管理外。リセットします。");
            g_TicketLong = 0;
            if (g_PartialCloseDirection > 0) { // このロングに関連する部分決済だった場合
                g_PartialCloseSet = false;
                g_PartialCloseDirection = 0;
                g_PartialCloseTriggerPrice = 0;
            }
            // g_PendingLongDetected もリセットした方が良い場合があるが、ManagePendingOrders に任せる
        }
    }
    if (g_TicketShort != 0) {
        if (!OrderSelect(g_TicketShort, SELECT_BY_TICKET, MODE_TRADES) ||
            (OrderMagicNumber() != g_MagicNumber || (OrderType() != OP_SELL && OrderType() != OP_SELLLIMIT)) ) {
            LogInfo("管理中のショート注文/ポジション(Ticket:" + IntegerToString(g_TicketShort) + ")が見つからないか、EA管理外。リセットします。");
            g_TicketShort = 0;
            if (g_PartialCloseDirection < 0) { // このショートに関連する部分決済だった場合
                g_PartialCloseSet = false;
                g_PartialCloseDirection = 0;
                g_PartialCloseTriggerPrice = 0;
            }
        }
    }
    // --- ポジション状態監視ここまで ---

    if(IsForceCloseTime())
    {
        CloseAllPositions(g_Symbol); // この中で部分決済フラグもリセットされる
        LogInfo("強制決済時間: すべてのポジションを決済しました");
        return;
    }
    
    ManagePendingOrders(g_Symbol); // 指値注文の管理 (約定検知、タイムアウトキャンセル)
    
    if(UsePartialClose)
    {
        CheckAndExecutePartialClose(g_Symbol); // 部分決済のチェックと実行
    }
    
    if(!IsTradeTime())
    {
        return;
    }
    
    if(!IsNewBar(g_Symbol, MainTimeFrame))
    {
        return;
    }
    
    if(g_TodayTrades >= MaxDailyTrades)
    {
        return;
    }
    
    // 既にEA管理のポジションがあるか、EA管理の指値注文がある場合は新規エントリーしない
    if(HasOpenPositionsByEA(g_Symbol) || HasPendingOrdersByEA(g_Symbol))
    {
        return;
    }
    
    // ... (以降の急騰検出、エントリーロジックは変更なし。ただし注文発行時はマジックナンバーを使用) ...

    // エントリーロジック内の注文発行後 (ロングの例)
    if(ticket > 0)
    {
        g_TicketLong = ticket; // 指値注文のチケットを保存
        g_LongOrderTime = TimeCurrent();
        g_PendingLongDetected = true; // 指値検出フラグ
        
        if(UsePartialClose)
        {
            // SetupPartialClose を呼び出し、EA側のグローバル変数 g_PartialCloseSet 等を設定
            SetupPartialClose(g_Symbol, ticket, entryPrice, adjustedStopLoss, g_SurgeDirection);
        }
        // ... (ログ出力など) ...
    }
    // ... (ショートも同様) ...
}

// 全ポジションを決済する関数 (EA管理のもの)
void CloseAllPositions(string symbol)
{
    bool success = true;
    
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            // EAが管理している注文/ポジションのみ対象
            if(OrderSymbol() == symbol && OrderMagicNumber() == g_MagicNumber)
            {
                int currentTicket = OrderTicket();
                bool closedOrDeleted = false;

                if(OrderType() == OP_BUY || OrderType() == OP_SELL)
                {
                    if(!OrderClose(currentTicket, OrderLots(), (OrderType()==OP_BUY ? Bid:Ask), 3, White))
                    {
                        LogError("ポジション決済エラー Ticket " + IntegerToString(currentTicket) + ": " + GetLastErrorText(GetLastError()));
                        success = false;
                    } else {
                        closedOrDeleted = true;
                    }
                }
                else if(OrderType() == OP_BUYLIMIT || OrderType() == OP_SELLLIMIT)
                {
                    if(!DeleteOrderWithRetry(currentTicket)) // Utilities.mqh の関数
                    {
                        LogError("指値注文キャンセルエラー Ticket " + IntegerToString(currentTicket) + ": " + GetLastErrorText(GetLastError()));
                        success = false;
                    } else {
                        closedOrDeleted = true;
                    }
                }

                if (closedOrDeleted) {
                    // 決済/キャンセルした注文が部分決済の管理対象だったかチェック
                    if (UsePartialClose && g_PartialCloseSet) {
                        if ((g_PartialCloseDirection > 0 && currentTicket == g_TicketLong) ||
                            (g_PartialCloseDirection < 0 && currentTicket == g_TicketShort)) {
                            LogInfo("強制決済により部分決済設定をリセット: Ticket " + IntegerToString(currentTicket));
                            g_PartialCloseSet = false;
                            g_PartialCloseDirection = 0;
                            g_PartialCloseTriggerPrice = 0;
                        }
                    }
                    // EA管理のチケット番号もリセット
                    if (currentTicket == g_TicketLong) g_TicketLong = 0;
                    if (currentTicket == g_TicketShort) g_TicketShort = 0;
                }
            }
        }
    }
    
    if(success)
    {
        LogInfo("EA管理のすべてのポジション/注文を決済/キャンセルしました");
    }
    // グローバルな部分決済フラグも念のためリセット (対象がなくなったので)
    // ただし、上のループで個別に対応しているので、これは冗長かもしれないが安全策
    g_PartialCloseSet = false;
    g_PartialCloseDirection = 0;
    g_PartialCloseTriggerPrice = 0;
    g_TicketLong = 0; // Ensure these are cleared
    g_TicketShort = 0;
}

// 指値注文を管理する関数
void ManagePendingOrders(string symbol)
{
    datetime currentTime = TimeCurrent();

    // ロング指値注文の管理
    if (g_PendingLongDetected && g_TicketLong != 0) {
        if (OrderSelect(g_TicketLong, SELECT_BY_TICKET, MODE_TRADES)) {
            if (OrderMagicNumber() == g_MagicNumber && OrderType() == OP_BUYLIMIT) {
                // タイムアウトチェック
                if (g_LongOrderTime > 0 && (currentTime - g_LongOrderTime) / 60 >= PendingOrderTimeout) {
                    if (DeleteOrderWithRetry(g_TicketLong)) {
                        LogInfo("ロング指値注文タイムアウトキャンセル: Ticket " + IntegerToString(g_TicketLong));
                        if (UsePartialClose && g_PartialCloseDirection > 0) { // この指値に関連する部分決済設定だった
                            g_PartialCloseSet = false;
                            g_PartialCloseDirection = 0;
                            g_PartialCloseTriggerPrice = 0;
                        }
                        g_TicketLong = 0;
                        g_PendingLongDetected = false;
                        g_LongOrderTime = 0;
                        ResetSurgeDetection(); // タイムアウトなら再検出から
                    } else {
                        LogError("ロング指値注文キャンセルエラー(タイムアウト): " + GetLastErrorText(GetLastError()));
                    }
                }
            } else { // チケットは存在するが、期待する指値注文ではない (約定したか、何らかの不整合)
                // 約定したかどうかをチェック (マジックナンバーでEAのポジションを探す)
                bool filled = false;
                int newTicket = 0;
                for (int i = OrdersTotal() - 1; i >= 0; i--) {
                    if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
                        if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol && OrderType() == OP_BUY &&
                            OrderOpenTime() >= g_LongOrderTime && (OrderOpenTime() - g_LongOrderTime < 300) ) { // 発注後5分以内に約定
                            filled = true;
                            newTicket = OrderTicket();
                            break;
                        }
                    }
                }
                if (filled) {
                    LogInfo("ロング指値(旧Ticket:" + IntegerToString(g_TicketLong) + ") 約定検知。新Ticket:" + IntegerToString(newTicket));
                    g_TicketLong = newTicket; // 約定後のポジションのチケットに更新
                    g_TodayTrades++;
                    g_LastTradeTime = TimeCurrent();
                    g_PendingLongDetected = false; // 指値ではなくなった
                    g_LongOrderTime = 0;
                    ResetSurgeDetection(); // 急騰検出からのサイクルは完了
                    // g_PartialCloseSet は SetupPartialClose で true になっているので維持
                } else {
                    // 約定せず、指値でもなくなった (手動キャンセル等)
                    LogInfo("ロング指値(Ticket:" + IntegerToString(g_TicketLong) + ") が存在しないかタイプ不正。リセット。");
                    if (UsePartialClose && g_PartialCloseDirection > 0) {
                        g_PartialCloseSet = false;
                        g_PartialCloseDirection = 0;
                        g_PartialCloseTriggerPrice = 0;
                    }
                    g_TicketLong = 0;
                    g_PendingLongDetected = false;
                    g_LongOrderTime = 0;
                }
            }
        } else { // OrderSelect失敗 = 指値注文が存在しない (約定したか、キャンセルされた)
            // ここでも約定検知を試みる (上記と同様のロジック)
            bool filled = false;
            int newTicket = 0;
            // ... (上記と同様の約定検知ループ) ...
            // 簡略化のため、OrderSelect失敗時は「なくなった」とみなし、関連フラグをリセット
            LogInfo("ロング指値(Ticket:" + IntegerToString(g_TicketLong) + ") OrderSelect失敗。リセット。");
            if (UsePartialClose && g_PartialCloseDirection > 0) {
                g_PartialCloseSet = false;
                g_PartialCloseDirection = 0;
                g_PartialCloseTriggerPrice = 0;
            }
            g_TicketLong = 0;
            g_PendingLongDetected = false;
            g_LongOrderTime = 0;
        }
    }

    // ショート指値注文の管理 (ロングと同様のロジック)
    if (g_PendingShortDetected && g_TicketShort != 0) {
        // ... (ロングと同様の処理) ...
        if (OrderSelect(g_TicketShort, SELECT_BY_TICKET, MODE_TRADES)) {
            if (OrderMagicNumber() == g_MagicNumber && OrderType() == OP_SELLLIMIT) {
                if (g_ShortOrderTime > 0 && (currentTime - g_ShortOrderTime) / 60 >= PendingOrderTimeout) {
                    if (DeleteOrderWithRetry(g_TicketShort)) {
                        LogInfo("ショート指値注文タイムアウトキャンセル: Ticket " + IntegerToString(g_TicketShort));
                        if (UsePartialClose && g_PartialCloseDirection < 0) {
                            g_PartialCloseSet = false;
                            g_PartialCloseDirection = 0;
                            g_PartialCloseTriggerPrice = 0;
                        }
                        g_TicketShort = 0;
                        g_PendingShortDetected = false;
                        g_ShortOrderTime = 0;
                        ResetSurgeDetection();
                    } else {
                        LogError("ショート指値注文キャンセルエラー(タイムアウト): " + GetLastErrorText(GetLastError()));
                    }
                }
            } else {
                bool filled = false;
                int newTicket = 0;
                for (int i = OrdersTotal() - 1; i >= 0; i--) {
                    if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
                        if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol && OrderType() == OP_SELL &&
                            OrderOpenTime() >= g_ShortOrderTime && (OrderOpenTime() - g_ShortOrderTime < 300)) {
                            filled = true;
                            newTicket = OrderTicket();
                            break;
                        }
                    }
                }
                if (filled) {
                    LogInfo("ショート指値(旧Ticket:" + IntegerToString(g_TicketShort) + ") 約定検知。新Ticket:" + IntegerToString(newTicket));
                    g_TicketShort = newTicket;
                    g_TodayTrades++;
                    g_LastTradeTime = TimeCurrent();
                    g_PendingShortDetected = false;
                    g_ShortOrderTime = 0;
                    ResetSurgeDetection();
                } else {
                    LogInfo("ショート指値(Ticket:" + IntegerToString(g_TicketShort) + ") が存在しないかタイプ不正。リセット。");
                    if (UsePartialClose && g_PartialCloseDirection < 0) {
                        g_PartialCloseSet = false;
                        g_PartialCloseDirection = 0;
                        g_PartialCloseTriggerPrice = 0;
                    }
                    g_TicketShort = 0;
                    g_PendingShortDetected = false;
                    g_ShortOrderTime = 0;
                }
            }
        } else {
            LogInfo("ショート指値(Ticket:" + IntegerToString(g_TicketShort) + ") OrderSelect失敗。リセット。");
            if (UsePartialClose && g_PartialCloseDirection < 0) {
                g_PartialCloseSet = false;
                g_PartialCloseDirection = 0;
                g_PartialCloseTriggerPrice = 0;
            }
            g_TicketShort = 0;
            g_PendingShortDetected = false;
            g_ShortOrderTime = 0;
        }
    }
}

// EA管理のオープンポジションがあるか (マジックナンバーで判定)
bool HasOpenPositionsByEA(string symbol)
{
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == symbol && OrderMagicNumber() == g_MagicNumber && 
               (OrderType() == OP_BUY || OrderType() == OP_SELL))
            {
                return true;
            }
        }
    }
    return false;
}

// EA管理の未決済の指値注文があるか (マジックナンバーで判定)
bool HasPendingOrdersByEA(string symbol)
{
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == symbol && OrderMagicNumber() == g_MagicNumber && 
               (OrderType() == OP_BUYLIMIT || OrderType() == OP_SELLLIMIT))
            {
                return true;
            }
        }
    }
    return false;
}
<一部割愛>

変更点の要約と理由:

  1. マジックナンバー: EAの注文/ポジションを確実に識別し、他のEAや手動取引と区別します。これにより、状態管理の信頼性が向上します。
  2. g_TicketLong/g_TicketShort: 指値注文発注時にそのチケットを保存し、約定時には新しいポジションのチケットに更新します。これにより、常にアクティブな注文/ポジションを追跡できます。
  3. g_PartialCloseSet 他:
    • SetupPartialClose で指値注文時に設定。
    • CheckAndExecutePartialClose で部分決済成功時にリセット。また、監視対象のポジションが存在しなくなった場合(SL/TP、手動決済など)にもリセット。
    • CloseAllPositions で強制決済時にリセット。
    • ManagePendingOrders で指値注文がタイムアウト等でキャンセルされた場合にリセット。約定時は維持。
  4. 約定検知: ManagePendingOrders 内で、指値注文のチケットが存在しなくなった場合に、マジックナンバーを頼りに該当する新しいポジションを探し、g_TicketLong/g_TicketShort を更新します。
  5. HasOpenPositionsByEA / HasPendingOrdersByEA: マジックナンバーを使ってEA管理の注文/ポジションのみを判定するヘルパー関数を追加。
  6. フラグリセットのタイミング: 各種リセット関数 (ResetDailyCounters, ResetSurgeDetection) や、ポジション・注文のライフサイクルイベント(決済、キャンセル)で、関連するフラグを適切にリセットするようにしました。

この修正により、部分決済ロジックはEAが管理するアクティブなポジションに対してのみ機能し、g_PartialCloseSet フラグはより正確にその状態を反映するようになります。特にマジックナンバーの導入と、約定時のチケット番号の更新が、この種のEAの安定動作には不可欠です。

テストを十分に行い、ログ出力を活用して各状態遷移でのフラグの動きを確認してください。

GPT-4.1案

あなたの述べた内容は「分割決済(Partial Close)の制御ロジックが意図通りに機能していない、また設計的にも一本足りない」という問題意識です。

現状では、「約定済みポジションがない場合にはg_PartialCloseSetがfalseにされる」ため、約定後に意図通り分割決済が働かない」→ これは即応的な不具合。
「強制決済時や指値エントリー後のキャンセル時にはg_PartialCloseSetをfalseにする必要」→ これは制御漏れを引き起こしやすく状態の一元管理が難しい設計

**理想は、「約定後、かつパラメータUsePartialCloseがtrueの時のみ分割決済が成立し、決済終了/注文キャンセル/強制決済等ではクリアされるという、状態遷移が明瞭なロジック」**にすべきというご意見です。
【現状ロジックの問題点(要約)】
g_PartialCloseSetはPartial Close必要性の全フラグとして使われているが、そのON/OFFの契機が注文の状態の全遷移イベントを正しくカバーできていない。
指値注文のキャンセルや全決済時でも残ることがあり、「Partial Closeができるべき時にできない」「必要ない時に残る」事がある。
ロジックが分岐散在し可読性・保守性が悪い。
【推奨する改修方針】

  1. Partial Closeフラグ設計の原則整理
    「約定済みポジション」かつ「UsePartialClose == true」かつ「その決済前」のみで、Partial Closeが働く状態フラグを持つ
    「未約定・キャンセル・全決済」時はPartial Closeフラグと関連情報が全てクリアされる
    状態変更ポイントをStrictに「全ての約定・注文削除・強制決済の入口で必ずPartial Close Stateをクリアorセット」に集中させる

  2. Partial Closeの管理情報の一元化(状態管理関数を作る)
    // Partial Close関連情報一括クリア
    void ResetPartialCloseState() {
    g_PartialCloseSet = false;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection = 0;
    g_TicketLong = 0;
    g_TicketShort = 0;
    }
    またはg_PartialCloseSetは不要で「PartialClose有効条件分岐を計算値で」でも良い。

  3. 状態遷移ごとの確実な処理
    (A) 注文約定時

ポジション約定と同時にSetupPartialClose(...)で必要情報セット
UsePartialClose == trueのみPartial Close有効。不要ならResetPartialCloseState()
(B) 指値注文キャンセル時・強制全決済時

注文チケット削除、全決済時の直後など必ずResetPartialCloseState()を呼ぶ
(C) 未約定状態でPartial Close判定をしない

CheckAndExecutePartialClose()は「注文が約定済かつPartialClose条件成立している場合のみPartialClose実施」
【手順例: 主要修正点】

  1. リセット関数実装
    // RiskManagement.mqh 最下部に
    void ResetPartialCloseState()
    {
    g_PartialCloseSet = false;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection = 0;
    }

  2. セット側ロジックの明瞭化
    // SetupPartialCloseを必ず「約定直後のみ」呼ぶ設計に統一
    // 約定しない場合・UsePartialClose==falseなら何もしない、逆に確定でPartial決済発動しないなら毎回Reset
    if(UsePartialClose)
    SetupPartialClose(...);
    else
    ResetPartialCloseState();

  3. 注文キャンセル時・全決済時は確実にリセット
    // 全ポジション決済 CloseAllPositions()/注文キャンセル(DeleteOrderWithRetry)/指値注文有効期限切れ などで
    ResetPartialCloseState();
    具体箇所
    CloseAllPositions(string symbol) の forループ終了後、成功・失敗問わずResetPartialCloseState呼び出し
    ManagePendingOrders のタイムアウトキャンセルや注文がもうない場合も ResetPartialCloseState呼び出し
    OnTickの該当部分も
    強制決済等直後

  4. CheckAndExecutePartialCloseの前提判定厳格化
    void CheckAndExecutePartialClose(string symbol)
    {
    if(!UsePartialClose || !g_PartialCloseSet) return; // 既存通り

    // 追加: 該当約定ポジションが本当に存在するか再チェック
    int ticket = (g_PartialCloseDirection > 0) ? g_TicketLong : g_TicketShort;
    if(ticket<=0 || !OrderSelect(ticket, SELECT_BY_TICKET) || (OrderType()!=OP_BUY && OrderType()!=OP_SELL)) {
    // チケット無効→リセット
    ResetPartialCloseState();
    return;
    }
    // ...以降、既存のまま分割決済判定へ
    }
    【まとめ】これで得られるメリット
    状態フラグの残留・消失によるバグがなくなる(確実なリセット、確実なセット)
    Partial Closeが発動してほしいときだけちゃんと発動する
    状態遷移が明瞭になり、保守性・機能追加耐性が向上
    【実装例 修正diffイメージ(抜粋)】
    RiskManagement.mqh
    // 末尾などに追加
    void ResetPartialCloseState()
    {
    g_PartialCloseSet = false;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection = 0;
    }
    EURUSD_Surge_EA.mq4 (EURUSD_Surge_EA.txt)
    // 注文のタイムアウトやキャンセル時
    if(DeleteOrderWithRetry(g_TicketLong)) {
    ...
    ResetPartialCloseState(); // ← 部分決済状態も必ずリセット
    }

// 強制全決済関連の最後や注文キャンセル系も必ず…
CloseAllPositions(g_Symbol);
ResetPartialCloseState();
CheckAndExecutePartialClose側
void CheckAndExecutePartialClose(string symbol)
{
if(!UsePartialClose || !g_PartialCloseSet) return;

int ticket = (g_PartialCloseDirection > 0) ? g_TicketLong : g_TicketShort;
if(ticket <= 0 || !OrderSelect(ticket, SELECT_BY_TICKET) || (OrderType()!=OP_BUY && OrderType()!=OP_SELL)) {
    ResetPartialCloseState();
    return;
}
// …以降は現状通り

}
総評
全ての状態遷移点で「Partial Close関連状態」が「約定後且つPartial利用時のみON、注文キャンセルや全決済時必ずOFF」となるよう一元的な管理関数で厳格に制御するのが最良です。
(部分決済に必要な管理情報の初期化・セットも分かりやすくなります)

これで、「必要な時のみ」分割決済が機能し、「不要時には漏れなくクリアされる」ロバストな維持管理ができます。

もし実コードでの具体的な変更案を全文でほしい場合、その旨追加リクエストください!

Claude案

部分決済の問題点をよく特定されましたね。現在のコードでは、約定前にg_PartialCloseSetフラグが誤ってfalseになり、正しく部分決済が実行されない状況になっています。また、このフラグ管理が他の状況(強制決済や指値キャンセル時)でも適切に行われるべきという点もご指摘の通りです。

この問題を根本的に解決するため、部分決済の実装を完全に見直す改修案を提案します。

改修案:チケットベースの部分決済管理システム

現在のフラグベースの実装から、各ポジションを個別に追跡する実装に変更します。

//+------------------------------------------------------------------+
//| グローバル変数の追加(RiskManagement.mqhの先頭付近に追加)           |
//+------------------------------------------------------------------+
int g_PartialClosedTickets[100]; // 部分決済済みチケットを記録する配列
int g_PartialClosedCount = 0;    // 部分決済済みチケットの数

//+------------------------------------------------------------------+
//| 部分決済の設定(リスク管理セクション内の既存関数を置き換え)          |
//+------------------------------------------------------------------+
void SetupPartialClose(string symbol, int ticket, double entryPrice, double stopLoss, int direction)
{
    if(!UsePartialClose)
    {
        return;
    }
    
    // 設定時には何もせず、全ての処理はCheckAndExecutePartialCloseで行う
    LogInfo("部分決済機能有効: チケット #" + IntegerToString(ticket) + " のセットアップ完了");
}

//+------------------------------------------------------------------+
//| 部分決済条件をチェックして実行(リスク管理セクション内の既存関数を置き換え)|
//+------------------------------------------------------------------+
void CheckAndExecutePartialClose(string symbol)
{
    // 部分決済が無効ならすぐに終了
    if(!UsePartialClose)
    {
        return;
    }
    
    // オープンポジションをチェック
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
            continue;
            
        // 対象通貨ペアのポジションのみ処理
        if(OrderSymbol() != symbol)
            continue;
            
        // 買いまたは売りポジションのみ処理(指値注文は除外)
        if(OrderType() != OP_BUY && OrderType() != OP_SELL)
            continue;
            
        int ticket = OrderTicket();
        
        // 既に部分決済済みかチェック
        bool alreadyPartiallyClosed = false;
        for(int j = 0; j < g_PartialClosedCount; j++)
        {
            if(g_PartialClosedTickets[j] == ticket)
            {
                alreadyPartiallyClosed = true;
                break;
            }
        }
        
        if(alreadyPartiallyClosed)
            continue;
            
        // ポジションの方向を判定
        int direction = (OrderType() == OP_BUY) ? 1 : -1;
        
        // 現在の価格を取得
        double currentPrice = (direction > 0) ? Bid : Ask;
        
        // 部分決済トリガー価格を計算
        double stopLoss = OrderStopLoss();
        double entryPrice = OrderOpenPrice();
        double riskDistance = MathAbs(entryPrice - stopLoss);
        double triggerDistance = riskDistance * PartialCloseTrigger;
        double triggerPrice;
        
        if(direction > 0) // ロング
        {
            triggerPrice = NormalizeDouble(entryPrice + triggerDistance, Digits);
            // トリガー条件をチェック
            if(currentPrice >= triggerPrice)
            {
                ExecutePartialClose(ticket, OrderLots(), direction, symbol);
            }
        }
        else // ショート
        {
            triggerPrice = NormalizeDouble(entryPrice - triggerDistance, Digits);
            // トリガー条件をチェック
            if(currentPrice <= triggerPrice)
            {
                ExecutePartialClose(ticket, OrderLots(), direction, symbol);
            }
        }
    }
}

//+------------------------------------------------------------------+
//| 部分決済を実行する関数(新規追加)                                  |
//+------------------------------------------------------------------+
void ExecutePartialClose(int ticket, double lots, int direction, string symbol)
{
    // 部分決済するロットサイズを計算
    double closeVolume = lots * (PartialClosePercent / 100.0);
    closeVolume = NormalizeDouble(closeVolume, 2);
    
    // 最小ロットサイズを確認
    double minLot = MarketInfo(symbol, MODE_MINLOT);
    if(closeVolume < minLot)
    {
        LogWarning("部分決済ロットが最小ロット未満のためスキップします: " + 
                   DoubleToString(closeVolume, 2) + " < " + DoubleToString(minLot, 2));
        return;
    }
    
    // 部分決済を実行
    bool result = false;
    if(direction > 0) // ロング
    {
        result = OrderClose(ticket, closeVolume, Bid, 3, Green);
    }
    else // ショート
    {
        result = OrderClose(ticket, closeVolume, Ask, 3, Red);
    }
    
    if(result)
    {
        LogInfo("部分決済実行: チケット #" + IntegerToString(ticket) + 
               ", ロット=" + DoubleToString(closeVolume, 2));
        
        // 部分決済済みチケットとして記録
        if(g_PartialClosedCount < 100)
        {
            g_PartialClosedTickets[g_PartialClosedCount] = ticket;
            g_PartialClosedCount++;
        }
    }
    else
    {
        LogError("部分決済エラー: " + GetLastErrorText(GetLastError()));
    }
}

//+------------------------------------------------------------------+
//| 部分決済状態を初期化する関数(新規追加)                            |
//+------------------------------------------------------------------+
void InitPartialCloseSystem()
{
    g_PartialClosedCount = 0;
    ArrayInitialize(g_PartialClosedTickets, 0);
}

さらに、メイン関数(OnInit)と日次カウンターリセット関数にも変更を加えます:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // 既存の初期化コードはそのまま
    
    // 部分決済システムの初期化を追加
    InitPartialCloseSystem();
    
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 日次カウンターのリセット関数                                      |
//+------------------------------------------------------------------+
void ResetDailyCounters()
{
    g_TodayTrades = 0;
    g_LastTradeTime = 0;
    ResetSurgeDetection();
    
    // 部分決済状態もリセット
    InitPartialCloseSystem();
}

改修のポイント

  1. チケットベースの管理: 各ポジションのチケット番号を記録し、部分決済済みのポジションを追跡します。

  2. リアルタイム計算: 部分決済条件を約定後の実際のポジションに対してリアルタイムに計算するため、フラグに依存せず常に正確な判断ができます。

  3. 多重決済防止: 一度部分決済したポジションは記録され、二度と部分決済されないようになります。

  4. シンプルな状態管理: 約定前後の複雑なフラグ管理が不要になり、メンテナンス性が向上します。

  5. 複数ポジション対応: 各ポジションを個別に処理するため、複数のポジションがある場合も正しく動作します。

この改修により、既存の問題点(約定前のフラグリセット、フラグ管理の複雑さ)を解消し、より堅牢な部分決済機能を実現できます。また、強制決済や日次リセット時にも適切に状態がリセットされるため、一貫した動作が期待できます。

o3案

Partial‑Close Logic Refactor Plan

1  現状の問題点と原因分析

症状 原因となるコード 問題の本質
約定前に g_PartialCloseSetfalse に戻り、約定後に分割決済できない RiskManagement.mqh → CheckAndExecutePartialClose()OrderSelect() が未約定(OP_BUYLIMIT/OP_SELLLIMIT)を選択した際に "約定済みではない" と判定し g_PartialCloseSet = false; を実行 “未約定=不要” という短絡的判断。約定後にフラグが必要なことを考慮していない
強制決済・タイムアウト・全決済後にもフラグが生き残るケースがある CloseAllPositions()ManagePendingOrders() でフラグをリセットしていない フラグ管理が分散し、統一された状態遷移がない

問題の根源

  • ブール値 1 bit で 4 段階以上の状態を表現しているため取り消しタイミングが曖昧
 → 明確なステートマシンを導入するべき。
  • 機能横断の 状態リセット処理が各所に散在 している
 → リセット責務を 1 箇所に集約。

2  改修方針

  1. 状態を列挙型で管理  enum PartialCloseState { PC_NONE, PC_WAITING_FILL, PC_ACTIVE, PC_DONE };
  2. 状態遷移テーブル で明示的に管理し、副作用を排除。
  3. ResetPartialCloseState() ユーティリティを追加し、全モジュールがそれを呼び出す。
  4. CheckAndExecutePartialClose()PC_ACTIVE でのみ動作。実行完了後 PC_DONE へ。
  5. ManagePendingOrders() で約定確認時 PC_WAITING_FILL → PC_ACTIVE、未約定キャンセル時 → PC_NONE
  6. **CloseAllPositions()** と **強制決済** では ResetPartialCloseState()`。
  7. UsePartialClose == false の場合は常に PC_NONE とみなす。

列挙型で状態遷移を明示することで「どのタイミングで何をすべきか」が一目瞭然になり、今後の機能追加も安全になります。

3  モジュール別修正概要

3‑1 Globals 定義(EURUSD_Surge_EA.mq4)

// 旧: bool g_PartialCloseSet;
// 新規:
enum PartialCloseState { PC_NONE, PC_WAITING_FILL, PC_ACTIVE, PC_DONE };
PartialCloseState g_PartialCloseState = PC_NONE;

3‑2 RiskManagement.mqh

  • SetupPartialClose()

    • g_PartialCloseState = PC_WAITING_FILL;
  • CheckAndExecutePartialClose()

    • 冒頭を if(!UsePartialClose || g_PartialCloseState != PC_ACTIVE) return;
    • 実行成功後 g_PartialCloseState = PC_DONE;
    • フラグを自動で false にしない

3‑3 ManagePendingOrders()

  • 約定判定ブロックで g_PartialCloseState == PC_WAITING_FILL なら g_PartialCloseState = PC_ACTIVE;
  • タイムアウトで注文削除したとき g_PartialCloseState = PC_NONE;

3‑4 CloseAllPositions()IsForceCloseTime()

  • 汎用関数 ResetPartialCloseState() を呼び出して g_PartialCloseState = PC_NONE;

3‑5 ResetSurgeDetection() & ResetDailyCounters()

  • 同上で ResetPartialCloseState() を呼ぶ。

3‑6 ユーティリティ追加(Utilities.mqh)

void ResetPartialCloseState()
{
    g_PartialCloseState = PC_NONE;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection   = 0;
}

4  代表的な状態遷移図

           +-----------------------+
           |                       |
           |   SetupPartialClose   |
           |                       |
           +-----------+-----------+
                       |
                       v
         +-------------+--------------+
         |  PC_WAITING_FILL (未約定)  |
         +------+------+-------------+
                | 約定           | キャンセル/タイムアウト
                v                v
    +-----------+---+    +-------+-------+
    | PC_ACTIVE     |    | PC_NONE       |
    | (分割決済監視) |    | (リセット)   |
    +----+------+---+    +---------------+
         | 分割決済完了/全決済
         v
   +-----+-----------------+
   | PC_DONE (監視終了)    |
   +-----------------------+

5  テストシナリオ

  1. 正常経路 : 指値 → 約定 → 利確が PartialCloseTrigger 到達 → 分割決済実行 → PC_DONE へ。
  2. 指値キャンセル : 有効時間経過で削除 → PC_NONE へ。
  3. 強制決済 : 指定時間で全決済 → PC_NONE へ。
  4. 全決済後に価格が再度到達 : PC_NONE のため重複決済は起きない。

6  移行手順

  1. 既存プロジェクトにバックアップタグを切る。
  2. 上記変更を適用しコンパイル。
  3. Strategy Tester で 3 年分のヒストリカルを実行し、ログにエラーが無いか確認。
  4. デモ口座で 1 週間フォワードテスト → 分割決済が正しく行われ、強制決済/タイムアウトでも残留フラグが無いことを確認。

7  将来的な拡張

  • ステートマシンは enum + switch で実装しているため、TP2/TP3 など多段分割決済やトレーリングストップへの拡張も容易。
  • PartialCloseManager として別モジュール化すると SRP (単一責任原則) が保たれ、メンテナンス性が向上。

ご不明点や追加要件があればコメントください。

Gemini最終案

かしこまりました。OpenAI o3 (GPT-3.5 Turbo または同様のレベルのモデルを指していると仮定します。あるいは GPT-4 の初期バージョンかもしれません) の提案したステートマシンベースのアプローチは非常に優れています。

ご指摘の「マジックナンバーとの連携」「g_TicketLong/g_TicketShort のクリア」「PC_DONE から PC_NONE への遷移」の考慮事項を盛り込み、より完全な形で具体的なソースコードとしてアウトプットします。

前提:

  • g_MagicNumberOnInit で適切に初期化されているものとします。
  • g_Symbol はEAが動作する通貨ペアです。
  • 各種 extern 変数(UsePartialClose, PartialClosePercent, PartialCloseTrigger など)は定義済みとします。

改修後のソースコード案

1. EURUSD_Surge_EA.mq4 (または .txt) のグローバル変数定義と OnInit

//+------------------------------------------------------------------+
//|                                              EURUSD_Surge_EA.mq4 |
//|                                                                  |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2025 Massan All Rights Reserved."
#property link      "https://github.com/Masa1984a/EURUSD_Surge_EA"
#property version   "1.01" // バージョンアップ
#property description "EURUSD専用の急騰急落検出型EA。ステートマシンによる高度な部分決済管理を導入。" // 説明更新
#property strict

// 外部ファイルのインクルード
#include "MarketAnalysis.mqh"
#include "EntryManagement.mqh"
#include "RiskManagement.mqh"
#include "FilteringSystem.mqh"
#include "Utilities.mqh"

// ... (既存の extern 変数定義はそのまま) ...

// 取引時間設定
extern string Time_Settings = "===== 取引時間設定 =====";
extern int    TRADE_START_HOUR = 8;
extern int    TRADE_END_HOUR = 22;
extern int    FORCE_CLOSE_HOUR = 23;
extern int    FORCE_CLOSE_MINUTE = 30;
extern int    PendingOrderTimeout = 120;

// --- 部分決済ステートマシン関連 ---
enum PartialCloseState {
    PC_NONE,           // 部分決済なし、または完了・リセット済み
    PC_WAITING_FILL,   // 部分決済対象の指値注文が約定待ち
    PC_ACTIVE,         // 部分決済対象のポジションがアクティブで、トリガー監視中
    PC_DONE            // 部分決済実行済み(同一ポジションに対しては再実行しない)
};
PartialCloseState g_PartialCloseState = PC_NONE; // 部分決済の現在の状態
double            g_PartialCloseTriggerPrice = 0; // 部分決済トリガー価格
int               g_PartialCloseDirection = 0;  // 部分決済方向 (1=ロング, -1=ショート)
// g_TicketLong と g_TicketShort は、部分決済対象の注文/ポジションのチケットとしても利用

// グローバル変数
string g_Symbol = "EURUSD";
int g_TodayTrades = 0;
datetime g_LastTradeTime = 0;
int g_TicketLong = 0;                         // ロング注文/ポジションのチケット番号
int g_TicketShort = 0;                        // ショート注文/ポジションのチケット番号
// bool g_PendingLongDetected = false;        // PC_WAITING_FILL で代替できるためコメントアウトまたは削除検討
// bool g_PendingShortDetected = false;       // 同上
datetime g_SurgeStartTime = 0;
double g_SurgeStartPrice = 0;
double g_SurgeEndPrice = 0;
int g_SurgeDirection = 0;
int g_AdjustmentCount = 0;
bool g_SurgeDetected = false;
datetime g_LongOrderTime = 0;
datetime g_ShortOrderTime = 0;
// bool g_PartialCloseSet = false;           // g_PartialCloseState で管理するため不要
// double g_PartialCloseTriggerPrice = 0;    // 上で定義済み
// int g_PartialCloseDirection = 0;         // 上で定義済み

double g_pipPoint;
int g_MagicNumber; // EAのマジックナンバー (OnInitで初期化)

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    if(Symbol() != g_Symbol)
    {
        LogError("このEAは" + g_Symbol + "専用です。現在の通貨ペア: " + Symbol());
        return INIT_FAILED;
    }
    if(Period() != MainTimeFrame)
    {
        LogError("このEAは" + IntegerToString(MainTimeFrame) + "分足専用です。現在の時間足: " + IntegerToString(Period()));
        return INIT_FAILED;
    }
    g_pipPoint = Point;
    if(Digits == 5 || Digits == 3)
    {
        g_pipPoint = Point * 10;
    }

    // マジックナンバーの初期化 (例: 任意の値、またはより動的な生成方法)
    g_MagicNumber = 123456; // ここでユニークな値を設定
    MathSrand(TimeLocal()); // 乱数シード初期化
    // g_MagicNumber = (int)AccountInfoInteger(ACCOUNT_LOGIN) % 100000 + MathRand() % 10000; // より動的な例
    // if (g_MagicNumber == 0) g_MagicNumber = 77777;


    ResetDailyCounters(); // これには ResetPartialCloseStateFull() が含まれる

    LogInfo("EURUSD Surge EA 初期化完了 - V1.01 (State Machine)");
    LogInfo("マジックナンバー: " + IntegerToString(g_MagicNumber));
    LogInfo("ブローカー情報:");
    LogInfo("最小ストップロス距離: " + DoubleToString(MarketInfo(g_Symbol, MODE_STOPLEVEL), 0) + " ポイント");
    // ... (その他のブローカー情報ログ) ...
    
    return(INIT_SUCCEEDED);
}

2. Utilities.mqh に状態リセット関数を追加

//+------------------------------------------------------------------+
//|                                                    Utilities.mqh |
//+------------------------------------------------------------------+
// ... (既存の関数) ...

//+------------------------------------------------------------------+
//| 部分決済状態リセット関数                                            |
//+------------------------------------------------------------------+
// この関数は部分決済のロジック状態をリセットする
void ResetPartialCloseLogicState()
{
    if (g_PartialCloseState != PC_NONE) { // 状態がNONEでなければログ出力
        LogInfo("部分決済状態をリセット: " + EnumToString(g_PartialCloseState) + " -> PC_NONE");
    }
    g_PartialCloseState = PC_NONE;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection = 0;
    // g_TicketLong/Short は、この関数では直接リセットしない。
    // これらはEAが管理するアクティブな注文/ポジションのチケットであり、
    // 部分決済ロジックのリセットとは独立して管理されるべき。
    // 部分決済対象でなくなった場合に個別に0にする。
}

// この関数は、EAの主要なリセットイベント(日替わり、EA停止からの再開時など)で呼び出され、
// アクティブな部分決済対象チケットも含めて完全にリセットする
void ResetPartialCloseStateFull()
{
    ResetPartialCloseLogicState(); // まずロジック状態をリセット
    // EAが部分決済のために監視していたチケット情報もクリア
    // ただし、これが原因でアクティブなトレードの管理が失われないよう注意が必要
    // 通常、この関数が呼ばれるときは、アクティブなトレードがないか、
    // EAが全ての状態をリフレッシュする前提の時
    if (g_PartialCloseDirection > 0 && g_TicketLong != 0) {
        // LogInfo("完全リセットに伴い、ロング監視チケット " + IntegerToString(g_TicketLong) + " をクリア");
        // g_TicketLong = 0; // g_TicketLong は部分決済専用ではないので、ここではクリアしない方が安全。
                       // CloseAllPositions や ManagePendingOrders で個別に管理
    }
    if (g_PartialCloseDirection < 0 && g_TicketShort != 0) {
        // LogInfo("完全リセットに伴い、ショート監視チケット " + IntegerToString(g_TicketShort) + " をクリア");
        // g_TicketShort = 0; // 同上
    }
    // LogInfo("部分決済状態を完全にリセットしました。");
}

補足: ResetPartialCloseLogicState は部分決済の「設定」をリセットし、ResetPartialCloseStateFull はより広範なリセットを意図していますが、g_TicketLong/Short のクリアは慎重に行うべきです。これらはEAが現在アクティブに追跡している注文/ポジション全体のチケットでもあるため、部分決済ロジックのリセットのみでこれらを0にすると、他の処理に影響が出る可能性があります。ここでは、ResetPartialCloseLogicState に絞り、チケットのクリアは各イベントハンドラで行う方が安全かもしれません。下のコードでは、この考え方を採用します。

より安全なリセット関数:

// Utilities.mqh
void ResetPartialCloseLogicState()
{
    if (g_PartialCloseState != PC_NONE) {
        LogInfo("部分決済状態をリセット: " + EnumToString(g_PartialCloseState) + " -> PC_NONE. 監視チケット: Long=" + IntegerToString(g_TicketLong) + ", Short=" + IntegerToString(g_TicketShort));
    }
    g_PartialCloseState = PC_NONE;
    g_PartialCloseTriggerPrice = 0;
    g_PartialCloseDirection = 0;
    // g_TicketLong と g_TicketShort はここではクリアしない。
    // 部分決済の対象でなくなった場合に、呼び出し元で必要に応じて 0 にする。
    // 例: g_TicketLong が部分決済対象であり、かつそのロングポジションが決済された場合など。
}

3. RiskManagement.mqh の修正

//+------------------------------------------------------------------+
//|                                               RiskManagement.mqh |
//+------------------------------------------------------------------+
// ... (CalculatePositionSize, CalculateDynamicStopLoss, CalculateTakeProfit などは変更なし) ...

//+------------------------------------------------------------------+
//| 部分決済関連の関数                                                |
//+------------------------------------------------------------------+

// 部分決済の設定 (指値注文発注後に呼び出される)
void SetupPartialClose(string symbol, int ticket, double entryPrice, double stopLoss, int direction)
{
    if(!UsePartialClose) // EAのパラメータで部分決済が無効なら何もしない
    {
        ResetPartialCloseLogicState(); //念のためリセット
        return;
    }

    // 既に何らかの部分決済がアクティブまたは完了している場合は、新しい設定をしない
    // (EAが一度に1つの部分決済プロセスのみを管理する場合)
    // もし複数の独立したトレードを同時に持つEAなら、このチェックは不要か、より複雑な管理が必要
    if (g_PartialCloseState != PC_NONE) {
        LogWarning("SetupPartialClose: 既に部分決済プロセスが進行中(" + EnumToString(g_PartialCloseState) + ")。新規セットアップをスキップ。 Ticket: " + IntegerToString(ticket));
        return;
    }
    
    // チケット番号を保存 (g_TicketLong/Short はEAメインで設定済みのはず)
    // ここでは方向に基づいて、どちらのチケットが監視対象かを確認する意味合いも持つ
    if(direction > 0)
    {
        if (g_TicketLong != ticket) {
            LogError("SetupPartialClose: ロング方向だが g_TicketLong(" + IntegerToString(g_TicketLong) + ") と渡された ticket(" + IntegerToString(ticket) +") が不一致。");
            return;
        }
    }
    else if (direction < 0)
    {
         if (g_TicketShort != ticket) {
            LogError("SetupPartialClose: ショート方向だが g_TicketShort(" + IntegerToString(g_TicketShort) + ") と渡された ticket(" + IntegerToString(ticket) +") が不一致。");
            return;
        }
    }
    else {
        LogError("SetupPartialClose: 無効な方向(" + IntegerToString(direction) + ")。");
        return;
    }

    // 部分決済トリガー価格を計算
    double riskDistance = MathAbs(entryPrice - stopLoss);
    if (riskDistance <= 0) {
        LogWarning("SetupPartialClose: リスク距離が0以下です。部分決済設定をスキップ。Entry=" + DoubleToString(entryPrice) + ", SL=" + DoubleToString(stopLoss));
        return;
    }
    double triggerDistance = riskDistance * PartialCloseTrigger; // EA側の PartialCloseTrigger
    
    if(direction > 0) // ロング
    {
        g_PartialCloseTriggerPrice = NormalizeDouble(entryPrice + triggerDistance, Digits);
    }
    else // ショート
    {
        g_PartialCloseTriggerPrice = NormalizeDouble(entryPrice - triggerDistance, Digits);
    }
    
    g_PartialCloseDirection = direction;
    g_PartialCloseState = PC_WAITING_FILL; // 指値注文なので約定待ち状態に
    
    LogInfo("部分決済設定: Ticket=" + IntegerToString(ticket) +
           ", トリガー価格=" + DoubleToString(g_PartialCloseTriggerPrice, Digits) + 
           ", 方向=" + IntegerToString(direction) + ", 状態 -> PC_WAITING_FILL");
}

// 部分決済条件をチェックして実行 (OnTickから毎ティック呼び出される)
void CheckAndExecutePartialClose(string symbol)
{
    if(!UsePartialClose || g_PartialCloseState != PC_ACTIVE) // 有効かつアクティブ状態でのみ実行
    {
        // PC_ACTIVE でない場合:
        // - PC_NONE: 何もしない
        // - PC_WAITING_FILL: 約定待ち (ManagePendingOrdersでPC_ACTIVEへ遷移させる)
        // - PC_DONE: 既に実行済み
        return;
    }
    
    int ticketToMonitor = 0;
    if(g_PartialCloseDirection > 0) ticketToMonitor = g_TicketLong;
    else if(g_PartialCloseDirection < 0) ticketToMonitor = g_TicketShort;

    if (ticketToMonitor == 0) {
        LogWarning("CheckAndExecutePartialClose(PC_ACTIVE): 監視対象チケットが0。状態をリセット。");
        ResetPartialCloseLogicState();
        return;
    }

    if (OrderSelect(ticketToMonitor, SELECT_BY_TICKET, MODE_TRADES))
    {
        // EAが発行した約定済みポジションか、かつ現在の部分決済状態と一致するか確認
        if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol &&
            ((OrderType() == OP_BUY && g_PartialCloseDirection > 0) || (OrderType() == OP_SELL && g_PartialCloseDirection < 0)) )
        {
            double currentPrice = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK);
            bool triggerCondition = false;

            if (OrderType() == OP_BUY)
            {
                triggerCondition = (currentPrice >= g_PartialCloseTriggerPrice && g_PartialCloseTriggerPrice != 0);
            }
            else // OP_SELL
            {
                triggerCondition = (currentPrice <= g_PartialCloseTriggerPrice && g_PartialCloseTriggerPrice != 0);
            }

            if (triggerCondition)
            {
                double initialLots = OrderLots();
                double closeVolume = NormalizeDouble(initialLots * (PartialClosePercent / 100.0), 2);
                double minLot = MarketInfo(OrderSymbol(), MODE_MINLOT);
                double lotStep = MarketInfo(OrderSymbol(), MODE_LOTSTEP);
                
                closeVolume = MathMax(minLot, MathFloor(closeVolume / lotStep) * lotStep);

                if (closeVolume >= minLot && closeVolume < initialLots) // 全決済にならないように
                {
                    bool result = false;
                    double closePrice = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK);
                    
                    result = OrderClose(ticketToMonitor, closeVolume, closePrice, 3, (OrderType()==OP_BUY ? Green:Red) );

                    if (result)
                    {
                        LogInfo("部分決済成功: Ticket=" + IntegerToString(ticketToMonitor) +
                               ", Lots=" + DoubleToString(closeVolume, 2) + "/" + DoubleToString(initialLots, 2) +
                               " at " + DoubleToString(closePrice, Digits) + ". 状態 -> PC_DONE");
                        g_PartialCloseState = PC_DONE; // 実行済み状態へ
                        // g_PartialCloseDirection や g_PartialCloseTriggerPrice は PC_DONE の間は保持しても良いし、クリアしても良い。
                        // ここでは保持しておくことで、後から何が起こったか追跡しやすくする。
                        // 次のトレードサイクルで ResetPartialCloseLogicState が呼ばれる際にクリアされる。
                    }
                    else
                    {
                        LogError("部分決済エラー: " + GetLastErrorText(GetLastError()) + " Ticket: " + IntegerToString(ticketToMonitor));
                        // エラー時はリトライしない方針なら状態は PC_ACTIVE のまま。リトライするなら別途考慮。
                        // ここでは一旦、失敗したら次のティックで再試行される(価格条件が合えば)
                    }
                }
                else if (closeVolume >= initialLots) {
                     LogInfo("部分決済スキップ(実質全決済): 計算ロット(" + DoubleToString(closeVolume,2) + ")が全ロット(" + DoubleToString(initialLots,2) + ")以上。状態 -> PC_DONE");
                     g_PartialCloseState = PC_DONE;
                }
                 else { // closeVolume < minLot
                    LogInfo("部分決済スキップ(最小ロット未満): 計算ロット(" + DoubleToString(closeVolume,2) + ")が最小ロット(" + DoubleToString(minLot,2) + ")未満。状態 -> PC_DONE");
                    g_PartialCloseState = PC_DONE; // このチケットに対してはもう部分決済しない
                 }
            }
        }
        else { // OrderSelectは成功したが、EAの管理外か、タイプが部分決済方向と不一致
            LogWarning("CheckAndExecutePartialClose(PC_ACTIVE): Ticket " + IntegerToString(ticketToMonitor) + " は監視対象外かタイプ不一致。リセット。 Magic=" + IntegerToString(OrderMagicNumber()) + " Type=" + IntegerToString(OrderType()));
            ResetPartialCloseLogicState();
            // 関連する g_TicketLong/Short もクリアすべきか検討。
            // ここでは、部分決済のロジックだけリセットし、チケットはEAのメインループでの管理に任せる。
        }
    }
    else // OrderSelect が false = ポジションが存在しない (決済済み or 存在しないチケット)
    {
        int error = GetLastError();
        if (error != ERR_NO_RESULT && error != 0) { // ERR_NO_RESULT は「注文が見つからない」
            LogWarning("CheckAndExecutePartialClose(PC_ACTIVE): OrderSelectエラー Ticket " + IntegerToString(ticketToMonitor) + ": " + GetLastErrorText(error));
        }
        LogInfo("CheckAndExecutePartialClose(PC_ACTIVE): Ticket " + IntegerToString(ticketToMonitor) + " が存在しないため部分決済状態をリセット。");
        ResetPartialCloseLogicState(); // ポジションがないなら監視終了
        // このチケットはもうないので、関連する g_TicketLong/Short を 0 にする。
        if (g_PartialCloseDirection > 0 && ticketToMonitor == g_TicketLong) g_TicketLong = 0;
        if (g_PartialCloseDirection < 0 && ticketToMonitor == g_TicketShort) g_TicketShort = 0;
    }
}

4. EURUSD_Surge_EA.mq4 (または .txt) の主要関数修正

// EURUSD_Surge_EA.mq4

// ... (OnInit は修正済み) ...

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(IsNewDay())
    {
        ResetDailyCounters(); // この中で ResetPartialCloseLogicState() が呼ばれる
        LogInfo("新しい日を検出: カウンターをリセットしました");
    }

    // --- ポジション状態監視とフラグ整合性チェック (PC_DONE から PC_NONE への遷移管理も含む) ---
    if (g_PartialCloseState == PC_DONE) {
        int ticketToCheck = 0;
        if (g_PartialCloseDirection > 0) ticketToCheck = g_TicketLong;
        else if (g_PartialCloseDirection < 0) ticketToCheck = g_TicketShort;

        if (ticketToCheck != 0) {
            if (!OrderSelect(ticketToCheck, SELECT_BY_TICKET, MODE_TRADES) || OrderMagicNumber() != g_MagicNumber) {
                // 部分決済実行済みのポジションが存在しなくなった (全決済された)
                LogInfo("部分決済完了済みポジション(Ticket:" + IntegerToString(ticketToCheck) + ")が決済されたため、状態を PC_DONE -> PC_NONE へリセット。");
                ResetPartialCloseLogicState();
                if (g_PartialCloseDirection > 0 && ticketToCheck == g_TicketLong) g_TicketLong = 0;
                if (g_PartialCloseDirection < 0 && ticketToCheck == g_TicketShort) g_TicketShort = 0;
            }
        } else { // PC_DONE なのに監視チケットがないのは矛盾。リセット。
            LogWarning("OnTick: PC_DONE で監視チケットが0。状態をリセット。");
            ResetPartialCloseLogicState();
        }
    }
    // --- ポジション状態監視ここまで ---


    if(IsForceCloseTime())
    {
        LogInfo("強制決済時間です。");
        CloseAllPositions(g_Symbol); // この中で ResetPartialCloseLogicState() が呼ばれる
        return;
    }
    
    ManagePendingOrders(g_Symbol); // 指値注文の管理 (約定検知、タイムアウトキャンセル、状態遷移)
    
    if(UsePartialClose) // パラメータが有効な場合のみチェック実行
    {
        CheckAndExecutePartialClose(g_Symbol); // 部分決済のチェックと実行
    }
    
    if(!IsTradeTime()) return;
    if(!IsNewBar(g_Symbol, MainTimeFrame)) return;
    if(g_TodayTrades >= MaxDailyTrades) return;
    
    // EA管理のオープンポジションがあるか、または部分決済対象の指値注文がある場合は新規エントリーしない
    // (g_PartialCloseState が PC_NONE 以外なら何らかの処理が進行中)
    if(HasOpenPositionsByEA(g_Symbol) || g_PartialCloseState != PC_NONE) // HasPendingOrdersByEA の代わりに状態を見る
    {
        if (g_PartialCloseState != PC_NONE) {
            // LogInfo("新規エントリー見送り: 部分決済プロセスが進行中 (" + EnumToString(g_PartialCloseState) + ")");
        }
        return;
    }
    
    // ... (急騰検出ロジック) ...
    if(!g_SurgeDetected)
    {
        // ... DetectPriceSurge ...
    }
    else // 急騰・急落後の調整と再エントリーポイントの検知
    {
        // ... (調整足ロジック) ...
        if(IsAdjustmentComplete(g_Symbol, MainTimeFrame, g_AdjustmentCount, g_SurgeDirection))
        {
            // ... (DetectReentryPoint ロジック) ...
            if(DetectReentryPoint(/* ... */))
            {
                // ... (lotSize 計算) ...
                int ticket = -1;
                
                if(g_SurgeDirection > 0 && EnableLongTrades) // ロング
                {
                    // ... (adjustedStopLoss 計算) ...
                    ticket = SendOrderWithRetry(OP_BUYLIMIT, lotSize, entryPrice, 3, adjustedStopLoss, takeProfit, 
                                              "EURUSD Surge EA", g_MagicNumber, 0, Green);
                    if(ticket > 0)
                    {
                        g_TicketLong = ticket; // EA管理のロング注文チケット
                        g_LongOrderTime = TimeCurrent();
                        // g_PendingLongDetected = true; // g_PartialCloseState で管理
                        
                        if(UsePartialClose)
                        {
                            // SetupPartialClose を呼び出し、状態を PC_WAITING_FILL に
                            SetupPartialClose(g_Symbol, ticket, entryPrice, adjustedStopLoss, g_SurgeDirection);
                        } else {
                            ResetPartialCloseLogicState(); // 部分決済を使わないなら状態はNONE
                        }
                        LogInfo("ロング指値注文設定: Ticket=" + IntegerToString(ticket) + // ...
                               (UsePartialClose ? ", 部分決済設定済み (PC_WAITING_FILL)" : ", 部分決済なし"));
                    }
                    // ... (エラーログ) ...
                }
                else if(g_SurgeDirection < 0 && EnableShortTrades) // ショート
                {
                    // ... (adjustedStopLoss 計算) ...
                    ticket = SendOrderWithRetry(OP_SELLLIMIT, lotSize, entryPrice, 3, adjustedStopLoss, takeProfit, 
                                              "EURUSD Surge EA", g_MagicNumber, 0, Red);
                    if(ticket > 0)
                    {
                        g_TicketShort = ticket; // EA管理のショート注文チケット
                        g_ShortOrderTime = TimeCurrent();
                        // g_PendingShortDetected = true; // g_PartialCloseState で管理

                        if(UsePartialClose)
                        {
                           SetupPartialClose(g_Symbol, ticket, entryPrice, adjustedStopLoss, g_SurgeDirection);
                        } else {
                           ResetPartialCloseLogicState();
                        }
                        LogInfo("ショート指値注文設定: Ticket=" + IntegerToString(ticket) + // ...
                               (UsePartialClose ? ", 部分決済設定済み (PC_WAITING_FILL)" : ", 部分決済なし"));
                    }
                    // ... (エラーログ) ...
                }
                
                if(ticket > 0)
                {
                    g_SurgeDetected = false; // 次のサージ検出へ
                }
            }
        }
    }
}

// 全ポジションを決済する関数 (EA管理のもの)
void CloseAllPositions(string symbol)
{
    LogInfo("CloseAllPositions: EA管理下の全注文/ポジションの決済を開始します。MagicNumber: " + IntegerToString(g_MagicNumber));
    bool itemClosedOrDeleted = false;
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == symbol && OrderMagicNumber() == g_MagicNumber)
            {
                int currentTicket = OrderTicket();
                int orderType = OrderType();
                bool success = false;

                if(orderType == OP_BUY || orderType == OP_SELL)
                {
                    if(OrderClose(currentTicket, OrderLots(), (orderType==OP_BUY ? Bid:Ask), 3, White)) {
                        LogInfo("ポジション決済成功: Ticket=" + IntegerToString(currentTicket) + ", Type=" + IntegerToString(orderType));
                        success = true;
                    } else {
                        LogError("ポジション決済エラー Ticket " + IntegerToString(currentTicket) + ": " + GetLastErrorText(GetLastError()));
                    }
                }
                else if(orderType == OP_BUYLIMIT || orderType == OP_SELLLIMIT)
                {
                    if(DeleteOrderWithRetry(currentTicket)) { // Utilities.mqh の関数
                        LogInfo("指値注文キャンセル成功: Ticket=" + IntegerToString(currentTicket) + ", Type=" + IntegerToString(orderType));
                        success = true;
                    } else {
                        LogError("指値注文キャンセルエラー Ticket " + IntegerToString(currentTicket) + ": " + GetLastErrorText(GetLastError()));
                    }
                }

                if (success) {
                    itemClosedOrDeleted = true;
                    // 決済/キャンセルした注文が部分決済の監視対象だったかチェック
                    if ((g_PartialCloseDirection > 0 && currentTicket == g_TicketLong) ||
                        (g_PartialCloseDirection < 0 && currentTicket == g_TicketShort)) {
                        // このチケットは部分決済の監視対象だったので、状態をリセット
                        LogInfo("決済/キャンセルされたTicket " + IntegerToString(currentTicket) + " は部分決済対象でした。状態をリセット。");
                        ResetPartialCloseLogicState();
                    }
                    // EA管理のチケット番号もリセット
                    if (currentTicket == g_TicketLong) g_TicketLong = 0;
                    if (currentTicket == g_TicketShort) g_TicketShort = 0;
                }
            }
        }
    }
    
    // ループ後、EA管理のアイテムが1つでも決済/削除されたら、念のため部分決済状態を最終リセット
    // (複数ポジションを同時に持たない前提なら、これで十分のはず)
    // もし何もクローズ/デリートされなくても、アクティブな部分決済対象がなければリセットする
    if (g_PartialCloseState != PC_NONE) { // 何らかの状態が残っている場合
        int activePartialTicket = 0;
        if (g_PartialCloseDirection > 0) activePartialTicket = g_TicketLong;
        else if (g_PartialCloseDirection < 0) activePartialTicket = g_TicketShort;

        if (activePartialTicket == 0 || !OrderSelect(activePartialTicket, SELECT_BY_TICKET) || OrderMagicNumber() != g_MagicNumber) {
            LogInfo("CloseAllPositions後、アクティブな部分決済対象が見当たらないため、状態をリセットします。");
            ResetPartialCloseLogicState();
        }
    }
    // Ensure tickets are cleared if they were part of partial close and are now gone
    if (g_PartialCloseDirection > 0 && g_TicketLong != 0 && (!OrderSelect(g_TicketLong, SELECT_BY_TICKET) || OrderMagicNumber() != g_MagicNumber)) g_TicketLong = 0;
    if (g_PartialCloseDirection < 0 && g_TicketShort != 0 && (!OrderSelect(g_TicketShort, SELECT_BY_TICKET) || OrderMagicNumber() != g_MagicNumber)) g_TicketShort = 0;


    LogInfo("CloseAllPositions: 処理完了。");
}

// 指値注文を管理する関数
void ManagePendingOrders(string symbol)
{
    if (g_PartialCloseState != PC_WAITING_FILL) { // 約定待ち状態でないなら、この関数での部分決済関連処理は不要
        // ただし、EAが管理する g_TicketLong/Short の指値がタイムアウト等でキャンセルされるケースは別途処理が必要
        // ここでは、部分決済が PC_WAITING_FILL のケースのみを重点的に扱う
    }

    datetime currentTime = TimeCurrent();

    // ロング指値注文の管理
    if (g_TicketLong != 0) { // EAがロング指値注文を管理中
        if (OrderSelect(g_TicketLong, SELECT_BY_TICKET, MODE_TRADES)) {
            if (OrderMagicNumber() == g_MagicNumber) {
                if (OrderType() == OP_BUYLIMIT) { // まだ指値注文
                    // タイムアウトチェック
                    if (g_LongOrderTime > 0 && (currentTime - g_LongOrderTime) / 60 >= PendingOrderTimeout) {
                        if (DeleteOrderWithRetry(g_TicketLong)) {
                            LogInfo("ロング指値注文タイムアウトキャンセル: Ticket " + IntegerToString(g_TicketLong));
                            if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection > 0) {
                                ResetPartialCloseLogicState(); // 部分決済対象だったのでリセット
                            }
                            g_TicketLong = 0;
                            g_LongOrderTime = 0;
                            // ResetSurgeDetection(); // 再検出から
                        } else {
                            LogError("ロング指値注文キャンセルエラー(タイムアウト): " + GetLastErrorText(GetLastError()));
                        }
                    }
                }
                else if (OrderType() == OP_BUY) { // 約定済み!
                    LogInfo("ロング指値(Ticket:" + IntegerToString(g_TicketLong) + ") 約定検知。");
                    g_TodayTrades++;
                    g_LastTradeTime = TimeCurrent();
                    // g_PendingLongDetected = false; // 状態管理へ
                    g_LongOrderTime = 0; // 指値の発注時間はクリア
                    // ResetSurgeDetection(); // サージからのサイクルは完了なのでリセット

                    if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection > 0 && g_TicketLong == OrderTicket()) {
                        LogInfo("部分決済状態を PC_WAITING_FILL -> PC_ACTIVE へ遷移。");
                        g_PartialCloseState = PC_ACTIVE; // 約定したのでアクティブ状態へ
                    } else if (g_PartialCloseState != PC_NONE) {
                        // 約定したが、部分決済状態が期待と異なる場合 (手動操作などがあったか?)
                        LogWarning("ロング約定時、部分決済状態が予期せぬ状態(" + EnumToString(g_PartialCloseState) + ")。リセット検討。");
                        // ResetPartialCloseLogicState(); // 安全のためリセットも検討
                    }
                }
                // else: 予期しないタイプ。ログ等で調査。
            } else { // MagicNumber不一致
                LogWarning("ManagePendingOrders: g_TicketLong(" + IntegerToString(g_TicketLong) + ") はEA管理外。チケットクリア。");
                g_TicketLong = 0; // EA管理外ならクリア
                if (g_PartialCloseState != PC_NONE && g_PartialCloseDirection > 0) ResetPartialCloseLogicState();
            }
        } else { // OrderSelect失敗 = 注文が存在しない
            LogInfo("ManagePendingOrders: g_TicketLong(" + IntegerToString(g_TicketLong) + ") が存在せず。チケットクリア。");
            if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection > 0) {
                ResetPartialCloseLogicState(); // 部分決済対象だったのでリセット
            }
            g_TicketLong = 0;
            g_LongOrderTime = 0;
        }
    }

    // ショート指値注文の管理 (ロングと同様のロジック)
    if (g_TicketShort != 0) {
        if (OrderSelect(g_TicketShort, SELECT_BY_TICKET, MODE_TRADES)) {
             if (OrderMagicNumber() == g_MagicNumber) {
                if (OrderType() == OP_SELLLIMIT) {
                    if (g_ShortOrderTime > 0 && (currentTime - g_ShortOrderTime) / 60 >= PendingOrderTimeout) {
                        if (DeleteOrderWithRetry(g_TicketShort)) {
                            LogInfo("ショート指値注文タイムアウトキャンセル: Ticket " + IntegerToString(g_TicketShort));
                            if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection < 0) {
                                ResetPartialCloseLogicState();
                            }
                            g_TicketShort = 0;
                            g_ShortOrderTime = 0;
                            // ResetSurgeDetection();
                        } else {
                            LogError("ショート指値注文キャンセルエラー(タイムアウト): " + GetLastErrorText(GetLastError()));
                        }
                    }
                }
                else if (OrderType() == OP_SELL) { // 約定
                    LogInfo("ショート指値(Ticket:" + IntegerToString(g_TicketShort) + ") 約定検知。");
                    g_TodayTrades++;
                    g_LastTradeTime = TimeCurrent();
                    g_ShortOrderTime = 0;
                    // ResetSurgeDetection();
                    if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection < 0 && g_TicketShort == OrderTicket()) {
                        LogInfo("部分決済状態を PC_WAITING_FILL -> PC_ACTIVE へ遷移。");
                        g_PartialCloseState = PC_ACTIVE;
                    } else if (g_PartialCloseState != PC_NONE) {
                        LogWarning("ショート約定時、部分決済状態が予期せぬ状態(" + EnumToString(g_PartialCloseState) + ")。リセット検討。");
                    }
                }
            } else {
                LogWarning("ManagePendingOrders: g_TicketShort(" + IntegerToString(g_TicketShort) + ") はEA管理外。チケットクリア。");
                g_TicketShort = 0;
                if (g_PartialCloseState != PC_NONE && g_PartialCloseDirection < 0) ResetPartialCloseLogicState();
            }
        } else {
            LogInfo("ManagePendingOrders: g_TicketShort(" + IntegerToString(g_TicketShort) + ") が存在せず。チケットクリア。");
            if (g_PartialCloseState == PC_WAITING_FILL && g_PartialCloseDirection < 0) {
                ResetPartialCloseLogicState();
            }
            g_TicketShort = 0;
            g_ShortOrderTime = 0;
        }
    }
}


// 日次カウンターのリセット関数
void ResetDailyCounters()
{
    g_TodayTrades = 0;
    // g_LastTradeTime = 0; // これはリセットしない方が良い場合もある(前日の最後の取引からの経過時間を見たいなど)
                         // 今回のロジックでは特に使っていないのでどちらでも。
    
    // サージ検出状態をリセット
    // ResetSurgeDetection(); // これを呼ぶとg_TicketLong/Shortもクリアされる可能性があるので注意
                          // g_SurgeDetected, g_AdjustmentCount のみをリセットする方が安全か
    g_SurgeDetected = false;
    g_AdjustmentCount = 0;

    // 日替わり時には、アクティブな部分決済プロセスがないか確認し、なければ状態をリセット
    // ただし、日をまたいでポジションを保有し、部分決済がアクティブな場合は維持したい
    if (g_PartialCloseState != PC_ACTIVE && g_PartialCloseState != PC_WAITING_FILL) {
        // PC_ACTIVE や PC_WAITING_FILL の場合は、日をまたいでも継続
        LogInfo("ResetDailyCounters: 部分決済状態をリセット ("+EnumToString(g_PartialCloseState)+" -> PC_NONE)");
        ResetPartialCloseLogicState();
    } else {
        LogInfo("ResetDailyCounters: 部分決済状態 ("+EnumToString(g_PartialCloseState)+") は日をまたいで継続。");
    }
}

// EA管理のオープンポジションがあるか (マジックナンバーで判定)
bool HasOpenPositionsByEA(string symbol)
{
    // g_TicketLong や g_TicketShort が 0 でなく、かつそれが約定済みポジションなら true を返すように変更も検討
    if (g_TicketLong != 0) {
        if (OrderSelect(g_TicketLong, SELECT_BY_TICKET, MODE_TRADES)) {
            if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol && OrderType() == OP_BUY) return true;
        }
    }
    if (g_TicketShort != 0) {
        if (OrderSelect(g_TicketShort, SELECT_BY_TICKET, MODE_TRADES)) {
            if (OrderMagicNumber() == g_MagicNumber && OrderSymbol() == symbol && OrderType() == OP_SELL) return true;
        }
    }
    // 上記で見つからない場合、念のため全検索(EAが複数のポジションを同時に持てる設計の場合に有効)
    for(int i = 0; i < OrdersTotal(); i++)
    {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
            if(OrderSymbol() == symbol && OrderMagicNumber() == g_MagicNumber && 
               (OrderType() == OP_BUY || OrderType() == OP_SELL))
            {
                // g_TicketLong/Short がこのチケットと一致しない場合、EAの状態と取引履歴に不整合がある可能性
                // ここでは単純に存在すればtrueを返す
                return true;
            }
        }
    }
    return false;
}

// (ResetSurgeDetection は、g_TicketLong/Short や g_PartialCloseState に影響を与えないように修正が必要な場合がある)
// 現状の ResetSurgeDetection は多くのグローバル変数をリセットするため、
// 部分決済が進行中の場合は慎重に呼び出すか、リセットする項目を絞る。
// ここでは、OnTick内のエントリー判断ロジックで g_PartialCloseState != PC_NONE を条件に加えたため、
// ResetSurgeDetection の影響は限定的になっているはず。

主な変更点と設計思想:

  1. PartialCloseState enum: 部分決済のライフサイクルを4つの状態で明確に管理します (PC_NONE, PC_WAITING_FILL, PC_ACTIVE, PC_DONE)。
  2. g_PartialCloseState グローバル変数: 現在の部分決済状態を保持します。
  3. ResetPartialCloseLogicState(): 部分決済の論理状態(トリガー価格、方向、状態enum)を PC_NONE にリセットします。g_TicketLong/g_TicketShort は、この関数では直接クリアせず、各イベント(決済、キャンセル)発生時に呼び出し元が必要に応じてクリアします。これにより、これらのチケット変数が部分決済以外の目的(例:単にEAが管理するアクティブトレードの追跡)にも使われている場合の安全性を高めます。
  4. SetupPartialClose() (RiskManagement.mqh):
    • 指値注文発注後、UsePartialClosetrue なら、トリガー価格等を計算し、g_PartialCloseStatePC_WAITING_FILL に設定します。
    • 関連する g_TicketLong または g_TicketShort はEAメイン側で既にセットされている前提です。
  5. ManagePendingOrders() (EURUSD_Surge_EA.mq4):
    • 監視中の指値注文 (g_TicketLong または g_TicketShort) が約定したら、g_PartialCloseStatePC_WAITING_FILL であれば PC_ACTIVE に遷移させます。
    • 指値注文がタイムアウト等でキャンセルされた場合、g_PartialCloseStatePC_WAITING_FILL であれば ResetPartialCloseLogicState() を呼び出し、関連チケットを0にします。
  6. CheckAndExecutePartialClose() (RiskManagement.mqh):
    • g_PartialCloseStatePC_ACTIVE の場合のみ動作します。
    • トリガー条件を満たして部分決済が成功したら、g_PartialCloseStatePC_DONE に設定します。
    • 監視対象のポジションが存在しなくなった場合(例: SL/TPで全決済)、ResetPartialCloseLogicState() を呼び出し、関連チケットを0にします。
  7. OnTick() (EURUSD_Surge_EA.mq4):
    • PC_DONE 状態のポジションが完全に決済されたことを検知したら、ResetPartialCloseLogicState() を呼び出し、状態を PC_NONE に戻し、次のトレードサイクルに備えます。関連チケットも0にします。
    • 新規エントリー判断時、g_PartialCloseState != PC_NONE であれば、何らかの部分決済プロセスが進行中(または指値待ち)とみなし、新規エントリーを抑制します(EAが一度に1つのトレードサイクルのみを管理する前提)。
  8. CloseAllPositions() (EURUSD_Surge_EA.mq4):
    • EA管理下の全注文/ポジションを決済/キャンセルします。
    • 決済/キャンセルした注文が部分決済の監視対象であった場合、ResetPartialCloseLogicState() を呼び出し、関連チケットを0にします。
    • 処理の最後に、アクティブな部分決済対象が残っていないことを確認し、必要であれば状態をリセットします。
  9. ResetDailyCounters() (EURUSD_Surge_EA.mq4):
    • 日替わり時に、部分決済がアクティブ (PC_ACTIVE または PC_WAITING_FILL) でない限り、ResetPartialCloseLogicState() を呼び出します。これにより、日をまたぐアクティブなトレードの部分決済は継続されます。
  10. マジックナンバー: 全ての注文発行時、および注文選択・確認時にマジックナンバー g_MagicNumber を使用して、EAが管理する注文/ポジションであることを保証します。

この設計により、部分決済の状態遷移がより明確になり、各イベント(注文、約定、決済、キャンセル、時間経過など)に対して状態が適切に更新されるため、ロジックの堅牢性と保守性が向上します。

テストに関する重要な注意:
この規模の変更後は、ストラテジーテスターでのバックテストはもちろんのこと、デモ口座でのフォワードテストを十分に行い、ログを詳細に確認して、意図した通りに状態が遷移し、部分決済が実行(または実行されない)ことを徹底的に検証してください。特にエッジケース(例: 部分決済トリガー直前での強制決済、スリッページによる約定価格のズレなど)も考慮に入れると良いでしょう。

おわりに

o3案がとても良い評価になっていますが、体感としてはシステム要件定義や設計能力はたしかにo3が高い印象です。一方で、具体的なコーディングはGeminiの方が高い印象です。Geminiのみで改修案を考えてもらうか、o3で設計案を考えてもらいGeminiに実装案を考えてもらうのが、現時点で良さそうな印象でした。

Accenture Japan (有志)

Discussion