Skip to content

HOW PendingBreakoutOrchestrator WORKS - Detailed AnalysisΒΆ

🎯 Document Purpose¢

Show WHAT the orchestrator consists of and HOW EXACTLY it works at the code, methods and data level. Special attention is paid to the breakout detection logic and OCO mechanism.


πŸ“¦ What the orchestrator is made ofΒΆ

1. Class structure (lines 13-27)ΒΆ

public class PendingBreakoutOrchestrator
{

    // β”‚  SINGLE DEPENDENCY                      
    // └─────────────────────────────────────────
    private readonly MT5Service _service;


    // β”‚  6 CONFIGURABLE PARAMETERS              
    // └─────────────────────────────────────────
    public string Symbol { get; set; } = "EURUSD";
    public int BreakoutDistancePoints { get; set; } = 25;
    public int StopLossPoints { get; set; } = 15;
    public int TakeProfitPoints { get; set; } = 30;
    public double Volume { get; set; } = 0.01;
    public int MaxWaitMinutes { get; set; } = 30;


    // β”‚  DEPENDENCY INJECTION                   
    // └─────────────────────────────────────────
    public PendingBreakoutOrchestrator(MT5Service service)
    {
        _service = service;
    }
}

Dependency visualizationΒΆ

β”‚        PendingBreakoutOrchestrator                         
β”‚    
β”‚  β”‚  private readonly MT5Service _service                  
β”‚  └──────────────────────────┬───────────────────────────  
└─────────────────────────┼──────────────────────────────────
                          β”‚
                          β–Ό

        β”‚         MT5Service                  
        β”‚    
        β”‚  β”‚  private MT5Account _account    
        β”‚  └──────────────┬────────────────  
        └─────────────────┼────────────────────
                          β”‚
                          β–Ό

                β”‚      MT5Account         
                β”‚   
                β”‚  β”‚  gRPC Client        
                β”‚  └───────────────────  
                └─────────────────────────
                          β”‚
                          β–Ό
                    [MT5 Terminal]

πŸ”„ How ExecuteAsync() works - step by stepΒΆ

Phase 1: Initialization (lines 31-46)ΒΆ

public async Task<double> ExecuteAsync(CancellationToken ct = default)
{

    // β”‚  STEP 1: Output header                              
    // └─────────────────────────────────────────────────────
    Console.WriteLine("\n+============================================================+");
    Console.WriteLine("|  PENDING BREAKOUT ORCHESTRATOR                            |");
    Console.WriteLine("+============================================================+\n");


    // β”‚  STEP 2: Get initial balance                        
    // └─────────────────────────────────────────────────────
    var initialBalance = await _service.GetBalanceAsync();
    Console.WriteLine($"  Starting balance: ${initialBalance:F2}");


    // β”‚  STEP 3: Get current price                          
    // └─────────────────────────────────────────────────────
    var tick = await _service.SymbolInfoTickAsync(Symbol);
    Console.WriteLine($"  Current: Bid={tick.Bid:F5}, Ask={tick.Ask:F5}\n");
}

Phase 2: Placing BuyStop (lines 48-65)ΒΆ

// β”‚  PLACING BUY STOP (upward breakout)                     
// └─────────────────────────────────────────────────────────
Console.WriteLine("  Placing BUY STOP order...");

var buyStopResult = await _service.BuyStopPoints(
    symbol: Symbol,                         // "EURUSD"
    volume: Volume,                         // 0.01
    priceOffsetPoints: BreakoutDistancePoints,  // +25 (POSITIVE!)
    slPoints: StopLossPoints,               // 15
    tpPoints: TakeProfitPoints,             // 30
    comment: "Breakout-Buy"
);


// β”‚  RESULT CHECK                                           
// └─────────────────────────────────────────────────────────
if (buyStopResult.ReturnedCode != 10009)
{
    Console.WriteLine($"  βœ— BUY STOP failed: {buyStopResult.Comment}\n");
    return 0;  // ← EMERGENCY EXIT
}

Console.WriteLine($"  βœ“ BUY STOP placed: #{buyStopResult.Order}\n");

How BuyStopPoints() works internallyΒΆ

// MT5Sugar.cs (extension method)
public static async Task<OrderSendData> BuyStopPoints(
    this MT5Service service,
    string symbol,
    double volume,
    int priceOffsetPoints,  // ← RECEIVES +25
    int slPoints = 0,
    int tpPoints = 0,
    string comment = ""
)
{

    // β”‚  STEP 1: Get current Ask price                      
    // β”‚  For BUY STOP use ASK (buy price)                  
    // └─────────────────────────────────────────────────────
    var tick = await service.SymbolInfoTickAsync(symbol);
    double askPrice = tick.Ask;  // For example: 1.10002


    // β”‚  STEP 2: Get point size                             
    // └─────────────────────────────────────────────────────
    var symbolInfo = await service.SymbolInfoAsync(symbol);
    double point = symbolInfo.Point;  // For EURUSD: 0.00001

    // β”‚  STEP 3: Calculate BUY STOP price                   
    // β”‚  IMPORTANT: priceOffsetPoints POSITIVE (+25)        
    // β”‚  BUY STOP is placed ABOVE current price             
    // β”‚                                                     
    // β”‚  price = askPrice + (priceOffsetPoints Γ— point)     
    // β”‚       = 1.10002 + (25 Γ— 0.00001)                    
    // β”‚       = 1.10002 + 0.00025                           
    // β”‚       = 1.10027                                     
    // └─────────────────────────────────────────────────────
    double price = askPrice + (priceOffsetPoints * point);


    // β”‚  STEP 4: Calculate SL and TP for BUY                
    // β”‚  SL below entry price (protection from fall)        
    // β”‚  TP above entry price (profit taking)               
    // β”‚                                                     
    // β”‚  sl = price - (slPoints Γ— point)                    
    // β”‚     = 1.10027 - (15 Γ— 0.00001)                      
    // β”‚     = 1.10027 - 0.00015                             
    // β”‚     = 1.10012                                       
    // β”‚                                                     
    // β”‚  tp = price + (tpPoints Γ— point)                    
    // β”‚     = 1.10027 + (30 Γ— 0.00001)                      
    // β”‚     = 1.10027 + 0.00030                             
    // β”‚     = 1.10057                                       
    // └─────────────────────────────────────────────────────
    double sl = slPoints > 0 ? price - (slPoints * point) : 0;
    double tp = tpPoints > 0 ? price + (tpPoints * point) : 0;


    // β”‚  STEP 5: Call low-level method                      
    // └─────────────────────────────────────────────────────
    return await service.BuyStopAsync(
        symbol: symbol,      // "EURUSD"
        volume: volume,      // 0.01
        price: price,        // 1.10027
        sl: sl,              // 1.10012
        tp: tp,              // 1.10057
        comment: comment     // "Breakout-Buy"
    );
}

Complete call chain for BuyStopΒΆ

β”‚  USER CODE (PendingBreakoutOrchestrator.cs:50)                 
β”‚  await _service.BuyStopPoints(                                 
β”‚      symbol: "EURUSD",                                         
β”‚      volume: 0.01,                                             
β”‚      priceOffsetPoints: +25,  ← POINTS (POSITIVE)              
β”‚      slPoints: 15,                                             
β”‚      tpPoints: 30,                                             
β”‚      comment: "Breakout-Buy"                                   
β”‚  )                                                             
└──────────────────────────┬─────────────────────────────────────
                           β”‚
                           β–Ό

β”‚  MT5Sugar.BuyStopPoints() (extension method)                   
β”‚  
β”‚  β”‚ 1. Get Ask: 1.10002                                        
β”‚  β”‚ 2. Get point: 0.00001                                     
β”‚  β”‚ 3. Calculate price: 1.10002 + (25 Γ— 0.00001) = 1.10027    
β”‚  β”‚ 4. Calculate SL: 1.10027 - (15 Γ— 0.00001) = 1.10012      
β”‚  β”‚ 5. Calculate TP: 1.10027 + (30 Γ— 0.00001) = 1.10057      
β”‚  └────────────────────────────────────────────────────────── 
β”‚  await service.BuyStopAsync(                                   
β”‚      price: 1.10027,    ← ABSOLUTE PRICE                       
β”‚      sl: 1.10012,                                              
β”‚      tp: 1.10057                                               
β”‚  )                                                             
└──────────────────────────┬─────────────────────────────────────
                           β”‚
                           β–Ό

β”‚  MT5Service.BuyStopAsync()                                     
β”‚  return await _account.BuyStopAsync(...)                       
└──────────────────────────┬─────────────────────────────────────
                           β”‚
                           β–Ό

β”‚  MT5Account.BuyStopAsync()                                     
β”‚   
β”‚  β”‚ var request = new OrderSendRequest {                       
β”‚  β”‚     Symbol = "EURUSD",                                    
β”‚  β”‚     Volume = 0.01,                                         
β”‚  β”‚     Type = ORDER_TYPE_BUY_STOP,  // = 4                 
β”‚  β”‚     Price = 1.10027,                                     
β”‚  β”‚     Sl = 1.10012,                                          
β”‚  β”‚     Tp = 1.10057,                                         
β”‚  β”‚     Comment = "Breakout-Buy"                              
β”‚  β”‚ }                                                         
β”‚  └────────────────────────────────────────────────────────── 
β”‚  var response = await _client.OrderSendAsync(request);         
└──────────────────────────┬─────────────────────────────────────
                           β”‚
                           β–Ό

                    β”‚  gRPC NETWORK
                    └──────┬───────
                           β”‚
                           β–Ό

                  β”‚   MT5 Terminal     
                  β”‚   
                  β”‚  β”‚ Places       
                  β”‚  β”‚ BUY STOP       
                  β”‚  β”‚ @ 1.10027      
                  β”‚  β”‚ SL: 1.10012    
                  β”‚  β”‚ TP: 1.10057   
                  β”‚  β”‚ Ticket:        
                  β”‚  β”‚  123456789     
                  β”‚  └────────────── 
                  └────────┬───────────
                           β”‚
                           β–Ό

                    β”‚  RESPONSE    
                    β”‚ OrderSendData
                    β”‚ {            
                    β”‚   ReturnedCode
                    β”‚   = 10009,   
                    β”‚   Order =    
                    β”‚   123456789  
                    β”‚ }            
                    └──────────────

Phase 3: Placing SellStop (lines 67-86)ΒΆ

// β”‚  PLACING SELL STOP (downward breakout)                  
// └─────────────────────────────────────────────────────────
Console.WriteLine("  Placing SELL STOP order...");

var sellStopResult = await _service.SellStopPoints(
    symbol: Symbol,
    volume: Volume,
    priceOffsetPoints: -BreakoutDistancePoints,  // -25 (NEGATIVE!)
    slPoints: StopLossPoints,
    tpPoints: TakeProfitPoints,
    comment: "Breakout-Sell"
);


// β”‚  CRITICALLY IMPORTANT CHECK                             
// └─────────────────────────────────────────────────────────
if (sellStopResult.ReturnedCode != 10009)
{
    Console.WriteLine($"  βœ— SELL STOP failed: {sellStopResult.Comment}");
    Console.WriteLine("  Canceling BUY STOP...");


    // β”‚  IMPORTANT: Cancel first order!                 
    // β”‚  Otherwise hanging BuyStop remains without pair 
    // └─────────────────────────────────────────────────
    await _service.CloseByTicket(buyStopResult.Order);
    return 0;
}

Console.WriteLine($"  βœ“ SELL STOP placed: #{sellStopResult.Order}\n");

How SellStopPoints() works internallyΒΆ

// MT5Sugar.cs (extension method)
public static async Task<OrderSendData> SellStopPoints(
    this MT5Service service,
    string symbol,
    double volume,
    int priceOffsetPoints,  // ← RECEIVES -25
    int slPoints = 0,
    int tpPoints = 0,
    string comment = ""
)
{

    // β”‚  STEP 1: Get current Bid price                      
    // β”‚  For SELL STOP use BID (sell price)                 
    // └─────────────────────────────────────────────────────
    var tick = await service.SymbolInfoTickAsync(symbol);
    double bidPrice = tick.Bid;  // For example: 1.10000

    var symbolInfo = await service.SymbolInfoAsync(symbol);
    double point = symbolInfo.Point;  // 0.00001


    // β”‚  STEP 2: Calculate SELL STOP price                  
    // β”‚  IMPORTANT: priceOffsetPoints NEGATIVE (-25)        
    // β”‚  SELL STOP is placed BELOW current price            
    // β”‚                                                     
    // β”‚  price = bidPrice + (priceOffsetPoints Γ— point)     
    // β”‚       = 1.10000 + (-25 Γ— 0.00001)                   
    // β”‚       = 1.10000 - 0.00025                           
    // β”‚       = 1.09975                                     
    // └─────────────────────────────────────────────────────
    double price = bidPrice + (priceOffsetPoints * point);


    // β”‚  STEP 3: Calculate SL and TP for SELL               
    // β”‚  SL ABOVE entry price (protection from rise)        
    // β”‚  TP BELOW entry price (profit taking)               
    // β”‚                                                     
    // β”‚  sl = price + (slPoints Γ— point)  ← PLUS for SELL!  
    // β”‚     = 1.09975 + (15 Γ— 0.00001)                      
    // β”‚     = 1.09975 + 0.00015                             
    // β”‚     = 1.09990                                       
    // β”‚                                                    
    // β”‚  tp = price - (tpPoints Γ— point)  ← MINUS for SELL! 
    // β”‚     = 1.09975 - (30 Γ— 0.00001)                      
    // β”‚     = 1.09975 - 0.00030                             
    // β”‚     = 1.09945                                       
    // └─────────────────────────────────────────────────────
    double sl = slPoints > 0 ? price + (slPoints * point) : 0;  // + for SELL
    double tp = tpPoints > 0 ? price - (tpPoints * point) : 0;  // - for SELL

    return await service.SellStopAsync(
        symbol: symbol,
        volume: volume,
        price: price,    // 1.09975
        sl: sl,          // 1.09990
        tp: tp,          // 1.09945
        comment: comment
    );
}

Result of placing both ordersΒΆ

After successfully placing both orders in MT5 Terminal:

PENDING ORDERS (2 orders):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Ticket β”‚    Type     β”‚  Price  β”‚   SL    β”‚   TP    β”‚   Comment    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ ...789 β”‚ BUY STOP    β”‚ 1.10027 β”‚ 1.10012 β”‚ 1.10057 β”‚ Breakout-Buy β”‚
β”‚ ...790 β”‚ SELL STOP   β”‚ 1.09975 β”‚ 1.09990 β”‚ 1.09945 β”‚ Breakout-Sellβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

VISUALIZATION:
          ↑ Price rises
          β”‚
  1.10057 β”œβ”€β”€β”€ TP for BuyStop
  1.10027 β”œβ”€β”€β”€ BUY STOP (upward breakout)
  1.10012 β”œβ”€β”€β”€ SL for BuyStop
          β”‚
  1.10002 β”œβ”€β”€β”€ Current Ask
  1.10000 β”œβ”€β”€β”€ Current Bid
          β”‚
  1.09990 β”œβ”€β”€β”€ SL for SellStop
  1.09975 β”œβ”€β”€β”€ SELL STOP (downward breakout)
  1.09945 β”œβ”€β”€β”€ TP for SellStop
          β”‚
          ↓ Price falls

Phase 4: BREAKOUT MONITORING (lines 89-129)ΒΆ

This is the most important part of the orchestrator - the breakout detection logic.

Console.WriteLine($"  ⏳ Waiting up to {MaxWaitMinutes} minutes for breakout...\n");


// β”‚  MONITORING INITIALIZATION                              
// └─────────────────────────────────────────────────────────
var startTime = DateTime.UtcNow;
var timeout = TimeSpan.FromMinutes(MaxWaitMinutes);

// Variables to track which order executed
ulong? executedOrder = null;   // Ticket of executed order
ulong? cancelOrder = null;     // Ticket of order to cancel


// β”‚  MAIN MONITORING LOOP                                   
// └─────────────────────────────────────────────────────────
while (DateTime.UtcNow - startTime < timeout && !ct.IsCancellationRequested)
{

    // β”‚  Wait 3 seconds before next check              
    // └─────────────────────────────────────────────────
    await Task.Delay(3000, ct);


    // β”‚  STEP 1: Get list of PENDING orders             
    // β”‚  IMPORTANT: This is only PENDING orders!        
    // β”‚  Executed orders become positions              
    // └─────────────────────────────────────────────────
    var tickets = await _service.OpenedOrdersTicketsAsync();

    bool buyStillPending = false;
    bool sellStillPending = false;


    // β”‚  STEP 2: Check if our orders are in the list    
    // └─────────────────────────────────────────────────
    foreach (var ticket in tickets.OpenedOrdersTickets)
    {
        if (ticket == (long)buyStopResult.Order) buyStillPending = true;
        if (ticket == (long)sellStopResult.Order) sellStillPending = true;
    }


    // β”‚  STEP 3: BREAKOUT DETECTION (4 scenarios)           
    // └─────────────────────────────────────────────────────

    // ═══════════════════════════════════════════════════════
    // SCENARIO 1: UPWARD BREAKOUT
    // ═══════════════════════════════════════════════════════
    if (!buyStillPending && sellStillPending)
    {
        // BuyStop DISAPPEARED from list β†’ executed!
        // SellStop STILL in list β†’ not executed

        Console.WriteLine("  πŸš€ BUY STOP EXECUTED! Upward breakout!");
        executedOrder = buyStopResult.Order;
        cancelOrder = sellStopResult.Order;  // ← Need to cancel
        break;  // Exit loop
    }

    // ═══════════════════════════════════════════════════════
    // SCENARIO 2: DOWNWARD BREAKOUT
    // ═══════════════════════════════════════════════════════
    else if (buyStillPending && !sellStillPending)
    {
        // SellStop DISAPPEARED β†’ executed!
        // BuyStop STILL in list β†’ not executed

        Console.WriteLine("  πŸš€ SELL STOP EXECUTED! Downward breakout!");
        executedOrder = sellStopResult.Order;
        cancelOrder = buyStopResult.Order;  // ← Need to cancel
        break;
    }

    // ═══════════════════════════════════════════════════════
    // SCENARIO 3: BOTH EXECUTED (rare case)
    // ═══════════════════════════════════════════════════════
    else if (!buyStillPending && !sellStillPending)
    {
        // Both orders disappeared
        // Possible causes:
        // 1. Very strong volatility (both triggered)
        // 2. Connection error (didn't get actual data)
        // 3. Manual cancellation of both orders

        Console.WriteLine("  βœ“ Both orders executed or canceled");
        break;
    }

    // ═══════════════════════════════════════════════════════
    // SCENARIO 4: BOTH STILL PENDING (continue waiting)
    // ═══════════════════════════════════════════════════════
    // else: both orders still in list β†’ no breakout β†’ continue loop
}

Detailed breakout detection logic visualizationΒΆ

β”‚  HOW OpenedOrdersTicketsAsync() WORKS                      
└────────────────────────────────────────────────────────────

MT5 Terminal state:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PENDING ORDERS  β”‚  OPEN POSITIONS                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  789: BUY STOP   β”‚  (empty)                               β”‚
β”‚  790: SELL STOP  β”‚                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

OpenedOrdersTicketsAsync() returns:
{
    OpenedOrdersTickets: [789, 790],  ← PENDING ORDERS
    OpenedPositionTickets: []         ← OPEN POSITIONS
}

foreach (var ticket in tickets.OpenedOrdersTickets)
{
    if (ticket == 789) buyStillPending = true;   // βœ“ found
    if (ticket == 790) sellStillPending = true;  // βœ“ found
}

Result: buyStillPending = true, sellStillPending = true
β†’ Both orders still pending β†’ no breakout β†’ continue waiting

─────────────────────────────────────────────────────────────

5 MINUTES LATER: Price rose to 1.10027

MT5 Terminal state:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PENDING ORDERS  β”‚  OPEN POSITIONS                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  790: SELL STOP  β”‚  789: BUY 0.01 EURUSD @ 1.10027        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     ↑
                     BuyStop EXECUTED β†’ became POSITION!

OpenedOrdersTicketsAsync() returns:
{
    OpenedOrdersTickets: [790],       ← only SellStop
    OpenedPositionTickets: [789]      ← BuyStop became position
}

foreach (var ticket in tickets.OpenedOrdersTickets)
{
    // ticket = 790
    if (ticket == 789) buyStillPending = true;   // βœ— NOT found!
    if (ticket == 790) sellStillPending = true;  // βœ“ found
}

Result: buyStillPending = false, sellStillPending = true

if (!buyStillPending && sellStillPending)  ← TRUE!
{
    Console.WriteLine("πŸš€ BUY STOP EXECUTED! Upward breakout!");
    cancelOrder = 790;  ← Need to cancel SellStop
    break;
}

Phase 5: Canceling opposite order (lines 131-145)ΒΆ

// β”‚  CHECK: Did breakout happen?                            
// └─────────────────────────────────────────────────────────
if (cancelOrder.HasValue)
{
    // ═══════════════════════════════════════════════════════
    // BREAKOUT HAPPENED β†’ Cancel opposite order
    // ═══════════════════════════════════════════════════════

    Console.WriteLine($"  Canceling opposite order #{cancelOrder.Value}...");


    // β”‚  CloseByTicket - universal method:              
    // β”‚  - If ticket = pending order β†’ CANCEL           
    // β”‚  - If ticket = position β†’ CLOSE                 
    // └─────────────────────────────────────────────────
    await _service.CloseByTicket(cancelOrder.Value);

    Console.WriteLine("  βœ“ Canceled\n");
}
else
{
    // ═══════════════════════════════════════════════════════
    // TIMEOUT β†’ Breakout didn't happen β†’ Cancel BOTH orders
    // ═══════════════════════════════════════════════════════

    Console.WriteLine($"  ⏱ Timeout after {MaxWaitMinutes} minutes - canceling both orders...");


    // β”‚  Cancel both orders separately                 
    // └─────────────────────────────────────────────────
    await _service.CloseByTicket(buyStopResult.Order);
    await _service.CloseByTicket(sellStopResult.Order);

    Console.WriteLine("  βœ“ Both canceled\n");
}

How CloseByTicket() worksΒΆ

// MT5Sugar.cs (extension method)
public static async Task CloseByTicket(
    this MT5Service service,
    ulong ticket
)
{

    // β”‚  STEP 1: Check ticket type (order or position?)    
    // └─────────────────────────────────────────────────────

    // Attempt 1: Get as pending order
    var orders = await service.OrdersAsync();
    var order = orders.FirstOrDefault(o => o.Ticket == ticket);

    if (order != null)
    {

        // β”‚  This is PENDING ORDER β†’ CANCEL             
        // └─────────────────────────────────────────────
        await service.OrderDeleteAsync(ticket);
        return;
    }

    // Attempt 2: Get as open position
    var positions = await service.PositionsAsync();
    var position = positions.FirstOrDefault(p => p.Ticket == ticket);

    if (position != null)
    {

        // β”‚  This is OPEN POSITION β†’ CLOSE              
        // └─────────────────────────────────────────────
        await service.PositionCloseAsync(ticket);
        return;
    }

    // Ticket not found (already closed/canceled or doesn't exist)
}

Detailed order cancellation chainΒΆ

SITUATION: Upward breakout, need to cancel SellStop #790

USER CODE:
    await _service.CloseByTicket(790);
        β”‚
        β–Ό
MT5Sugar.CloseByTicket():

    β”‚ 1. OrdersAsync() β†’ check pending orders     
    β”‚ 2. Found order #790 β†’ this is PENDING ORDER 
    β”‚ 3. Call OrderDeleteAsync(790)               
    └─────────────────────────────────────────────
        β”‚
        β–Ό
MT5Service.OrderDeleteAsync(790):
    return await _account.OrderDeleteAsync(790);
        β”‚
        β–Ό
MT5Account.OrderDeleteAsync(790):
    var request = new OrderDeleteRequest {
        Ticket = 790
    };
    var response = await _client.OrderDeleteAsync(request);
        β”‚
        β–Ό
    gRPC β†’ MT5 Terminal
        β”‚
        β–Ό
MT5 Terminal:

    β”‚ PENDING ORDERS                          
    β”‚ βœ— 790: SELL STOP @ 1.09975 (DELETED)   
    └─────────────────────────────────────────

RESULT:
MT5 Terminal state:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PENDING ORDERS  β”‚  OPEN POSITIONS                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  (empty)         β”‚  789: BUY 0.01 EURUSD @ 1.10027        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Now only ONE position (BUY) is open.
SellStop successfully canceled β†’ OCO strategy completed.

Phase 6: Finalization (lines 147-154)ΒΆ

// β”‚  Get final balance                                      
// └─────────────────────────────────────────────────────────
var finalBalance = await _service.GetBalanceAsync();


// β”‚  Calculate profit/loss                                  
// └─────────────────────────────────────────────────────────
var profit = finalBalance - initialBalance;

Console.WriteLine($"  Final balance: ${finalBalance:F2}");
Console.WriteLine($"  Profit/Loss: ${profit:F2}");
Console.WriteLine("\n+============================================================+\n");


// β”‚  Return profit as result of ExecuteAsync()              
// └─────────────────────────────────────────────────────────
return profit;

🎭 Complete Life Cycle (Upward breakout scenario)¢

Execution timelineΒΆ

T=0      START ExecuteAsync()
         β”‚
         β”œβ”€β–Ί GetBalanceAsync()           β†’ $10000.00
         β”œβ”€β–Ί SymbolInfoTickAsync()       β†’ Bid:1.10000, Ask:1.10002
         β”‚
T=1s     β”œβ”€β–Ί BuyStopPoints(+25)
         β”‚   └─► Created BuyStop #789 @ 1.10027
         β”‚
T=2s     β”œβ”€β–Ί SellStopPoints(-25)
         β”‚   └─► Created SellStop #790 @ 1.09975
         β”‚
         β”‚   MT5 Terminal state:
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   β”‚ PENDING ORDERS β”‚ POSITIONS    β”‚
         β”‚   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
         β”‚   β”‚ 789: BUY STOP  β”‚ (empty)      β”‚
         β”‚   β”‚ 790: SELL STOP β”‚              β”‚
         β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
T=3s     β”œβ”€β–Ί MONITORING START
         β”‚
T=6s     β”œβ”€β–Ί Task.Delay(3000)
         β”‚   β”œβ”€β–Ί OpenedOrdersTicketsAsync() β†’ [789, 790]
         β”‚   β”œβ”€β–Ί buyStillPending = true
         β”‚   β”œβ”€β–Ί sellStillPending = true
         β”‚   └─► Both pending β†’ continue
         β”‚
T=9s     β”œβ”€β–Ί Task.Delay(3000)
         β”‚   └─► [789, 790] β†’ both pending β†’ continue
         β”‚
         β”‚   MARKET: Price rises...
         β”‚   1.10005 β†’ 1.10015 β†’ 1.10020 β†’ 1.10027...
         β”‚
T=12s    β”œβ”€β–Ί Task.Delay(3000)
         β”‚   β”‚
         β”‚   β”‚  MT5 Terminal: Price reached 1.10027!
         β”‚   β”‚  BuyStop #789 EXECUTED β†’ became position
         β”‚   β”‚
         β”‚   β”‚  MT5 Terminal state:
         β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   β”‚  β”‚ PENDING ORDERS β”‚ POSITIONS            β”‚
         β”‚   β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
         β”‚   β”‚  β”‚ 790: SELL STOP β”‚ 789: BUY 0.01        β”‚
         β”‚   β”‚  β”‚                β”‚      @ 1.10027       β”‚
         β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚   β”‚
         β”‚   β”œβ”€β–Ί OpenedOrdersTicketsAsync() β†’ [790]
         β”‚   β”œβ”€β–Ί buyStillPending = false  ← NOT found!
         β”‚   β”œβ”€β–Ί sellStillPending = true
         β”‚   β”‚
         β”‚   └─► if (!buy && sell) β†’ TRUE!
         β”‚       β”œβ”€β–Ί "πŸš€ BUY STOP EXECUTED! Upward breakout!"
         β”‚       β”œβ”€β–Ί executedOrder = 789
         β”‚       β”œβ”€β–Ί cancelOrder = 790
         β”‚       └─► break (exit loop)
         β”‚
T=13s    β”œβ”€β–Ί CANCEL opposite order
         β”‚   β”œβ”€β–Ί CloseByTicket(790)
         β”‚   β”‚   └─► OrderDeleteAsync(790)
         β”‚   β”‚       └─► SellStop #790 CANCELED
         β”‚   β”‚
         β”‚   β”‚  MT5 Terminal state:
         β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   β”‚  β”‚ PENDING ORDERS β”‚ POSITIONS            β”‚
         β”‚   β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
         β”‚   β”‚  β”‚ (empty)        β”‚ 789: BUY 0.01        β”‚
         β”‚   β”‚  β”‚                β”‚      @ 1.10027       β”‚
         β”‚   β”‚  β”‚                β”‚      SL: 1.10012     β”‚
         β”‚   β”‚  β”‚                β”‚      TP: 1.10057     β”‚
         β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚   OCO STRATEGY COMPLETED!
         β”‚   Position opened, opposite order canceled.
         β”‚
         β”‚   MARKET: Price continues rising...
         β”‚   1.10030 β†’ 1.10040 β†’ 1.10050 β†’ 1.10057...
         β”‚
T=180s   β”‚   MT5 Terminal: Price reached 1.10057!
(3 min)  β”‚   TP triggered β†’ position closed
         β”‚
         β”‚   MT5 Terminal state:
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   β”‚ PENDING ORDERS β”‚ POSITIONS            β”‚
         β”‚   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
         β”‚   β”‚ (empty)        β”‚ (empty)              β”‚
         β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚   CLOSED POSITION:
         β”‚   Entry: 1.10027
         β”‚   Exit:  1.10057
         β”‚   Profit: (1.10057 - 1.10027) Γ— 100000 Γ— 0.01
         β”‚          = 0.00030 Γ— 100000 Γ— 0.01
         β”‚          = 30 Γ— 0.01
         β”‚          = $3.00
         β”‚
T=183s   β”œβ”€β–Ί GetBalanceAsync()           β†’ $10003.00
         β”œβ”€β–Ί profit = 10003.00 - 10000.00 = +$3.00
         β”‚
         └─► RETURN profit = 3.00

πŸ“Š What the result is made ofΒΆ

Profit calculation (upward breakout)ΒΆ

INITIAL BALANCE: $10000.00

UPWARD BREAKOUT:

1. BuyStop #789 @ 1.10027 EXECUTED
   β†’ Opened position BUY 0.01 lots
   β†’ SL: 1.10012 (protection -15 points)
   β†’ TP: 1.10057 (target +30 points)

2. SellStop #790 CANCELED
   β†’ Cancellation fee: $0.00

POSITION OUTCOME (TP triggered):
   Entry: 1.10027
   Exit:  1.10057
   Pips:  (1.10057 - 1.10027) / 0.00001 = 30 points

PROFIT CALCULATION:
   Profit = Pips Γ— Point Value Γ— Volume
   Point Value for EURUSD (1.0 lot) = $10
   Point Value for 0.01 lot = $0.10

   Profit = 30 Γ— $0.10 = $3.00

FINAL BALANCE: $10003.00
PROFIT = $3.00

return 3.00;

Loss calculation (SL triggered)ΒΆ

IF SL HAD TRIGGERED:
   Entry: 1.10027
   Exit:  1.10012 (SL)
   Pips:  (1.10012 - 1.10027) / 0.00001 = -15 points

   Loss = -15 Γ— $0.10 = -$1.50

FINAL BALANCE: $9998.50
PROFIT = -$1.50

R:R ratio = 1.50 / 3.00 = 1:2 βœ“

🧩 Components and their roles¢

1. PendingBreakoutOrchestratorΒΆ

Role: OCO strategy coordinator

Tasks:

  • Stores parameters (Symbol, BreakoutDistancePoints, etc.)
  • Manages life cycle
  • Places both orders
  • Monitors breakout through OpenedOrdersTicketsAsync()
  • Cancels opposite order
  • Handles placement errors
  • Returns result

2. MT5ServiceΒΆ

Role: Service layer

Tasks:

  • Provides methods BuyStopAsync, SellStopAsync
  • Delegates calls to MT5Account

3. MT5Sugar (extension methods)ΒΆ

Role: Simplifying layer

Tasks:

  • BuyStopPoints() - converts points to prices for BuyStop
  • SellStopPoints() - converts points to prices for SellStop
  • CloseByTicket() - universal cancel/close

4. OpenedOrdersTicketsAsync()ΒΆ

Role: CRITICAL component for breakout detection

How it works:

// Returns TWO lists:
OpenedOrdersTicketsData {
    OpenedOrdersTickets: [list of pending order tickets],
    OpenedPositionTickets: [list of open position tickets]
}

// KEY MOMENT:
// When pending order executes β†’ it disappears from OpenedOrdersTickets
// and appears in OpenedPositionTickets

// Check ticket presence in pending list:
foreach (var ticket in tickets.OpenedOrdersTickets)
{
    if (ticket == ourBuyStopTicket) buyStillPending = true;
}

// If ticket NOT found β†’ order executed!
if (!buyStillPending) {
    // BuyStop no longer pending β†’ breakout happened!
}

πŸ” Final Dependency DiagramΒΆ

β”‚  USER CODE                                                   
β”‚  var orch = new PendingBreakoutOrchestrator(service);       
β”‚  var profit = await orch.ExecuteAsync();                    
└──────────────────────────┬──────────────────────────────────
                           β”‚
                           β–Ό

β”‚  PendingBreakoutOrchestrator                                
β”‚   
β”‚  β”‚  ExecuteAsync() {                                       
β”‚  β”‚    1. BuyStopPoints(+25)                                
β”‚  β”‚    2. SellStopPoints(-25)                               
β”‚  β”‚    3. LOOP: OpenedOrdersTicketsAsync()                  
β”‚  β”‚       └─► Check ticket presence                         
β”‚  β”‚    4. CloseByTicket(opposite)                           
β”‚  β”‚  }                                                      
β”‚  └───────────────────────────────────────────────────────  
└──────────────────────────┬──────────────────────────────────
                           β”‚ _service
                           β–Ό

β”‚  MT5Service                                                 
β”‚  - OpenedOrdersTicketsAsync()  ← KEY method!                
└──────────────────────────┬──────────────────────────────────
                           β”‚
                           β–Ό

β”‚  MT5Sugar (extension methods)                               
β”‚  - BuyStopPoints(+offsetPoints)  β†’ price ABOVE Ask          
β”‚  - SellStopPoints(-offsetPoints) β†’ price BELOW Bid          
β”‚  - CloseByTicket(ticket)         β†’ cancel/close             
└──────────────────────────┬──────────────────────────────────
                           β”‚
                           β–Ό
                    [MT5 Terminal]

🎯 Summary¢

PendingBreakoutOrchestrator is made of:

  1. 1 dependency: MT5Service _service
  2. 6 parameters: Symbol, BreakoutDistancePoints, StopLoss, TakeProfit, Volume, MaxWaitMinutes
  3. 4 MT5Sugar methods: BuyStopPoints, SellStopPoints, CloseByTicket, OpenedOrdersTicketsAsync
  4. OCO logic: Breakout detection through checking ticket presence in pending orders list

Works through:

  • Placing 2 opposite Stop orders
  • Monitoring pending orders list every 3 seconds
  • Detecting breakout: if ticket disappeared from list β†’ order executed
  • Canceling opposite order
  • Timeout handling

Returns:

  • double profit - difference between final and initial balance

Key insight:

All the magic of breakout detection is based on a simple fact: when a pending order executes, it disappears from the OpenedOrdersTickets list. By checking for our tickets' presence in this list, we know exactly which order triggered and which is still waiting for execution.