Page 6 of 9

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:45 am
by PTScalper
3. Wire Up OnStart and the Cleanup Event

When the bot boots, it must load the state immediately. Furthermore, you must subscribe to the Positions.Closed event to purge IDs from the dictionary when a trade hits its stop loss or is manually closed. Without this cleanup, your JSON file will bloat indefinitely.

Code: Select all

protected override void OnStart()
{
    InitializeStatePersistence();
    LoadState();
    
    // Subscribe to the global position closed event for memory cleanup
    Positions.Closed += OnPositionClosed;
}

private void OnPositionClosed(PositionClosedEventArgs args)
{
    // Only clean up targets belonging to this specific bot and symbol
    if (args.Position.Label == BotLabel && args.Position.SymbolName == SymbolName)
    {
        if (_scaleOutTargets.Remove(args.Position.Id))
        {
            // Save the state immediately after purging the closed ID
            SaveState();
            Print($"[State Cleaned] Purged Position ID {args.Position.Id} from state file.");
        }
    }
}

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:45 am
by PTScalper
4. Trigger SaveState() on Execution and Scale-Out

You must call SaveState() whenever the dictionary is modified.

Inside your Execution Method:
When a new trade is successfully opened and the target is calculated, save it.

Code: Select all

if (result.IsSuccessful)
{
    double targetPrice = tradeType == TradeType.Buy 
        ? result.Position.EntryPrice + (slPips * Symbol.PipSize * TargetRR)
        : result.Position.EntryPrice - (slPips * Symbol.PipSize * TargetRR);

    _scaleOutTargets[result.Position.Id] = Math.Round(targetPrice, Symbol.Digits);
    
    // Persist the new target to disk
    SaveState(); 
    
    Print($"[Scale Out Target Set] {targetPrice}");
}

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:46 am
by PTScalper
Inside your Scale-Out Logic (Ideally inside OnTick):

When the price hits your target and the 50% partial is successfully executed, remove the target so it doesn't trigger again, and update the file.

Code: Select all

TradeResult closeResult = ClosePosition(position, scaleOutVolume);

if (closeResult.IsSuccessful)
{
    // Remove from dictionary so it only scales out once
    _scaleOutTargets.Remove(position.Id);
    
    // Persist the removal to disk
    SaveState();
}

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:46 am
by PTScalper
Why File Serialization Beats Native Broker Workarounds

Some developers attempt to hijack the Position.Comment string upon execution to store the RR target price, parsing it back out on reboot. This avoids file I/O entirely but breaks down in institutional algorithms because brokers frequently overwrite position comments with server-side notes (e.g., "sl/tp modified", "partial close"). Serializing a strongly typed C# Dictionary to JSON guarantees your state remains uncorrupted by broker-side terminal updates.

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:46 am
by PTScalper
The worst way to handle a failed API call in an algorithmic trading bot is to wrap it in a while (!result.IsSuccessful) loop. Because cTrader operates synchronously on a single thread per bot, an infinite while-loop will freeze the bot, crash the platform, and potentially lock you out of managing the trade manually.

Because you are migrating the scale-out logic to the OnTick method, the market data feed itself acts as your natural retry loop. If a trade fails to close due to a requote, lack of liquidity, or temporary broker disconnect, the next incoming tick will evaluate the logic and try again automatically.

However, during high-volatility news events, you might receive 500 ticks per second. If your bot sends 500 ClosePosition requests in one second, your broker will permanently ban your API key for DDOSing their servers.

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:47 am
by PTScalper
The Institutional Cooldown Architecture

Add a timestamp dictionary to track the exact millisecond you last attempted to close a specific position.

Code: Select all

// --- State Tracking for Retries ---
private Dictionary<int, DateTime> _lastCloseAttempts = new Dictionary<int, DateTime>();

[Parameter("Retry Cooldown (ms)", DefaultValue = 500, Group = "Trade Management")]
public int RetryCooldownMs { get; set; }

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:47 am
by PTScalper
The OnTick Implementation

Move the scale-out logic completely out of OnBar and place it into OnTick. This ensures the profit is taken the millisecond the target is hit, and natively handles the retry loop with the cooldown limiter.

Code: Select all

protected override void OnTick()
{
    var position = Positions.Find(BotLabel, SymbolName);
    if (position == null) return;

    // 1. Check if the position has a registered scale-out target
    if (_scaleOutTargets.TryGetValue(position.Id, out double targetPrice))
    {
        // 2. Evaluate if the real market price has crossed the target
        bool isTargetHit = position.TradeType == TradeType.Buy 
            ? Symbol.Bid >= targetPrice 
            : Symbol.Ask <= targetPrice;

        if (isTargetHit)
        {
            // 3. COOLDOWN THROTTLE: Prevent API spam during requote storms
            if (_lastCloseAttempts.TryGetValue(position.Id, out DateTime lastAttempt))
            {
                if ((Server.Time - lastAttempt).TotalMilliseconds < RetryCooldownMs)
                {
                    return; // Skip this tick, cooldown has not expired
                }
            }

            double scaleOutVolume = Symbol.NormalizeVolumeInUnits(position.VolumeInUnits / 2, RoundingMode.Down);
            
            if (scaleOutVolume >= Symbol.VolumeInUnitsMin)
            {
                // Log the attempt time BEFORE firing the API call
                _lastCloseAttempts[position.Id] = Server.Time;

                Print($"[Attempting Scale-Out] Requesting {scaleOutVolume} units at {targetPrice}");
                TradeResult closeResult = ClosePosition(position, scaleOutVolume);

                // 4. RESULT HANDLING
                if (closeResult.IsSuccessful)
                {
                    Print($"[Scale-Out Success] 50% partial executed.");
                    
                    // Purge state and save to JSON
                    _scaleOutTargets.Remove(position.Id);
                    _lastCloseAttempts.Remove(position.Id);
                    SaveState(); 
                }
                else
                {
                    // API FAILED (Requote, Off-Quotes, Timeout, Margin)
                    // We DO NOT remove the target from _scaleOutTargets.
                    // The next tick that arrives after the RetryCooldownMs will try again.
                    Print($"[API Error] Scale-out failed: {closeResult.Error}. Retrying in {RetryCooldownMs}ms.");
                }
            }
            else
            {
                // Position is mathematically too small to divide further.
                // Remove the target so we stop checking.
                _scaleOutTargets.Remove(position.Id);
                SaveState();
            }
        }
    }
}

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:47 am
by PTScalper
Memory Cleanup for the Cooldown Tracker

Just as you clean up the JSON state file when a position completely closes, you must also purge the cooldown dictionary. Update your OnPositionClosed event handler to keep your memory footprint lean.

Code: Select all

private void OnPositionClosed(PositionClosedEventArgs args)
{
    if (args.Position.Label == BotLabel && args.Position.SymbolName == SymbolName)
    {
        // Clean up JSON persistence
        if (_scaleOutTargets.Remove(args.Position.Id))
        {
            SaveState();
        }
        
        // Clean up the cooldown memory
        _lastCloseAttempts.Remove(args.Position.Id);
    }
}

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:48 am
by PTScalper
Why This Is Bulletproof

The Requote Scenario: Price spikes to your target. The bot attempts to close. The broker returns TradeError.Requote. The if (closeResult.IsSuccessful) check fails. The target remains in memory. For the next 500 milliseconds, the bot ignores all incoming ticks. On tick 501, it attempts to close again, successfully securing the profit on the second try.

The Disconnect Scenario: The target is hit, but your VPS drops its connection to the broker right as the ClosePosition command fires. Because it fails, the JSON state file is never updated to remove the target. When your VPS reconnects to the broker 10 seconds later, the bot reloads the JSON, sees the target is still pending, checks the live price, and instantly fires the scale-out order.

Re: Heikin Ashi Candles, Smoothing the Noise

Posted: Sat Oct 03, 2026 5:48 am
by PTScalper
To execute a breakeven modification immediately after the partial close, you inject the API call directly inside the if (closeResult.IsSuccessful) block in your OnTick method.

A professional implementation must also account for two critical variables: commission coverage and direction safety (ensuring you do not accidentally move a stop loss backward if the Heikin Ashi trailing stop has already moved past the entry price).

1. Add the Breakeven Parameters

Add these inputs to the top of your class to control the feature from the cTrader UI.

Code: Select all

[Parameter("Move to Breakeven on Scale-Out", DefaultValue = true, Group = "Trade Management")]
public bool MoveToBreakeven { get; set; }

[Parameter("Breakeven Extra Buffer (Pips)", DefaultValue = 0.5, Group = "Trade Management", 
           ToolTip = "Small offset added to the entry price to cover broker commissions and swap costs.")]
public double BreakevenExtraPips { get; set; }