Page 4 of 7

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 9:58 am
by FTtrader
Here is the Pine Script v5 implementation featuring an on-chart dashboard to track your dynamic floors visually:

Code: Select all

//@version=5
strategy("Series Guardian Strategy", overlay=true, calc_on_every_tick=true, initial_capital=10000)

// --- Input Parameters ---
grp_goals = "Series Goals & Risk"
inpSeriesTarget  = input.float(12000.0, title="Series Equity Target", group=grp_goals)
inpTrailDrawdown = input.float(2500.0,  title="Trailing Drawdown Limit", group=grp_goals)
inpDailyDrawdown = input.float(1000.0,  title="Daily Hard Limit", group=grp_goals)

// --- State Variables ---
var float highWaterMark    = strategy.equity
var float startOfDayEquity = strategy.equity
var int   currentDay       = dayofyear(time)
var bool  isHalted         = false
var string haltReason      = ""

// --- Midnight Rollover Check ---
if dayofyear(time) != currentDay
    currentDay := dayofyear(time)
    startOfDayEquity := strategy.equity

// --- Update High-Water Mark ---
if strategy.equity > highWaterMark
    highWaterMark := strategy.equity

// --- Calculate Dynamic Floors ---
dynamicTrailingFloor = highWaterMark - inpTrailDrawdown
dynamicDailyFloor    = startOfDayEquity - inpDailyDrawdown
activeFloor          = math.max(dynamicTrailingFloor, dynamicDailyFloor)

// --- Guardian Logic (Target & Breach Detection) ---
if not isHalted
    // 1. Target Detection
    if inpSeriesTarget > 0 and strategy.equity >= inpSeriesTarget
        strategy.close_all(comment="SERIES COMPLETE")
        isHalted := true
        haltReason := "Target Reached: " + str.tostring(strategy.equity, "#.##")
        alert("SERIES COMPLETE: " + haltReason, alert.freq_all)
        
    // 2. Breach Detection
    else if strategy.equity <= activeFloor and strategy.opentrades > 0
        strategy.close_all(comment="FATAL BREACH")
        isHalted := true
        haltReason := "Floor Breached. Eq: " + str.tostring(strategy.equity, "#.##") + " Flr: " + str.tostring(activeFloor, "#.##")
        alert("FATAL ALERT: " + haltReason, alert.freq_all)

// --- Hypothetical Entry Logic (Only execute if series is active) ---
// Replace this with your actual M15/H4 entry conditions
longCondition = ta.crossover(ta.sma(close, 14), ta.sma(close, 28))
if longCondition and not isHalted
    strategy.entry("Long", strategy.long)

// --- Visual Dashboard ---
var table dashboard = table.new(position.top_right, 2, 5, border_width=1)
if barstate.islast
    table.cell(dashboard, 0, 0, "Current Equity:", text_color=color.white, bgcolor=color.gray)
    table.cell(dashboard, 1, 0, str.tostring(strategy.equity, "#.##"), text_color=color.white, bgcolor=color.gray)
    
    table.cell(dashboard, 0, 1, "Series Target:", text_color=color.white, bgcolor=color.green)
    table.cell(dashboard, 1, 1, str.tostring(inpSeriesTarget, "#.##"), text_color=color.white, bgcolor=color.green)
    
    table.cell(dashboard, 0, 2, "Active Floor:", text_color=color.white, bgcolor=color.red)
    table.cell(dashboard, 1, 2, str.tostring(activeFloor, "#.##"), text_color=color.white, bgcolor=color.red)
    
    table.cell(dashboard, 0, 3, "Drawdown Distance:", text_color=color.white, bgcolor=color.orange)
    table.cell(dashboard, 1, 3, str.tostring(strategy.equity - activeFloor, "#.##"), text_color=color.white, bgcolor=color.orange)
    
    if isHalted
        table.cell(dashboard, 0, 4, "STATUS:", text_color=color.white, bgcolor=color.black)
        table.cell(dashboard, 1, 4, "HALTED", text_color=color.red, bgcolor=color.black)

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 9:58 am
by FTtrader
Key Architectural Differences for TradingView

Intrabar Execution (calc_on_every_tick): By default, Pine Script only evaluates conditions when a candle closes. If you trade on the M15 or H1, a massive spike could blow past your drawdown limit intrabar. Setting calc_on_every_tick=true forces TradingView to evaluate the floors constantly during live price action.

The Halting Flag (isHalted): Pine Script has no equivalent to MQL5's ExpertRemove(). To stop the strategy from continuing to trade after hitting the series target or blowing the limit, you must wrap all your strategy.entry() conditions inside an if not isHalted check.

Resetting for a New Series: Because variables persist within the script instance, when you return from your 2 to 4-week break to start a new series, you must physically remove the strategy from the chart and re-add it (or modify a setting to force a script recompile) to reset the isHalted flag and high-water marks.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 9:59 am
by FTtrader
Transitioning this logic to cTrader allows you to leverage pure C# .NET. Instead of relying on archaic MT4 Global Variables, the state is persisted using cTrader's native LocalStorage API to ensure your 2 to 6-week series goals and daily rollovers survive VPS reboots.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 9:59 am
by FTtrader
Here is the production-ready Robot implementation:

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class SeriesGuardian : Robot
    {
        [Parameter("Series Target", DefaultValue = 12000.0, Group = "Risk Management")]
        public double SeriesTarget { get; set; }

        [Parameter("Trailing Drawdown Limit", DefaultValue = 2500.0, Group = "Risk Management")]
        public double TrailingDrawdown { get; set; }

        [Parameter("Daily Hard Limit", DefaultValue = 1000.0, Group = "Risk Management")]
        public double DailyDrawdown { get; set; }

        private double _highWaterMark;
        private double _startOfDayEquity;
        private int _currentDayOfYear;

        private string _gvMaxEqName;
        private string _gvSODEqName;
        private string _gvDayTrackerName;

        protected override void OnStart()
        {
            // Isolate state keys per account to prevent cross-contamination
            long login = Account.Number;
            _gvMaxEqName = $"MaxEq_{login}";
            _gvSODEqName = $"SODEq_{login}";
            _gvDayTrackerName = $"DayTrack_{login}";

            int today = Server.Time.DayOfYear;

            // 1. Recover or Initialize Trailing Watermark
            var savedMax = LocalStorage.GetString(_gvMaxEqName);
            if (!string.IsNullOrEmpty(savedMax) && double.TryParse(savedMax, out double maxEq))
            {
                _highWaterMark = maxEq;
            }
            else
            {
                _highWaterMark = Account.Equity;
                LocalStorage.SetString(_gvMaxEqName, _highWaterMark.ToString("F2"));
            }

            // 2. Recover or Initialize Daily Watermark
            var savedDay = LocalStorage.GetString(_gvDayTrackerName);
            var savedSOD = LocalStorage.GetString(_gvSODEqName);
            
            if (!string.IsNullOrEmpty(savedDay) && int.TryParse(savedDay, out int day) && day == today &&
                !string.IsNullOrEmpty(savedSOD) && double.TryParse(savedSOD, out double sodEq))
            {
                _startOfDayEquity = sodEq;
                _currentDayOfYear = today;
            }
            else
            {
                _startOfDayEquity = Account.Equity;
                _currentDayOfYear = today;
                LocalStorage.SetString(_gvSODEqName, _startOfDayEquity.ToString("F2"));
                LocalStorage.SetString(_gvDayTrackerName, _currentDayOfYear.ToString());
            }

            Print($"Guardian Active. Target: {SeriesTarget:F2} | Watermark: {_highWaterMark:F2} | SOD: {_startOfDayEquity:F2}");
        }

        protected override void OnTick()
        {
            double currentEquity = Account.Equity;
            int today = Server.Time.DayOfYear;

            // 1. Broker Midnight Rollover Check
            if (today != _currentDayOfYear)
            {
                _currentDayOfYear = today;
                _startOfDayEquity = currentEquity;
                LocalStorage.SetString(_gvSODEqName, _startOfDayEquity.ToString("F2"));
                LocalStorage.SetString(_gvDayTrackerName, _currentDayOfYear.ToString());
                Print($"Midnight Rollover: New Daily Baseline set to {_startOfDayEquity:F2}");
            }

            // 2. Series Target Detection (Evaluated before drawdown math)
            if (SeriesTarget > 0 && currentEquity >= SeriesTarget)
            {
                string targetMsg = $"SERIES COMPLETE: Goal reached! Equity: {currentEquity:F2}";
                Print(targetMsg);
                
                if (Positions.Count > 0) FlattenAllPositions();
                
                // Sanitize memory for the next series cycle
                ClearLocalStorage();
                Stop(); 
                return; 
            }

            // 3. Dynamically track and update the high-water mark
            if (currentEquity > _highWaterMark)
            {
                _highWaterMark = currentEquity;
                LocalStorage.SetString(_gvMaxEqName, _highWaterMark.ToString("F2"));
            }

            // 4. Calculate both physical floors
            double dynamicTrailingFloor = _highWaterMark - TrailingDrawdown;
            double dynamicDailyFloor = _startOfDayEquity - DailyDrawdown;
            double activeFloor = Math.Max(dynamicTrailingFloor, dynamicDailyFloor);

            // 5. Breach detection
            if (currentEquity <= activeFloor && Positions.Count > 0)
            {
                string alertMsg = $"FATAL ALERT: Floor breached! Equity: {currentEquity:F2}, Active Floor: {activeFloor:F2}";
                Print(alertMsg);
                
                FlattenAllPositions();
                ClearLocalStorage();
                Stop();
            }
        }

        private void FlattenAllPositions()
        {
            // Copy to array to avoid enumeration exceptions when collection modifies during closure
            var positionsToClose = Positions.ToArray();
            foreach (var position in positionsToClose)
            {
                ClosePosition(position);
            }
        }

        private void ClearLocalStorage()
        {
            LocalStorage.Remove(_gvMaxEqName);
            LocalStorage.Remove(_gvSODEqName);
            LocalStorage.Remove(_gvDayTrackerName);
        }
    }
}

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:00 am
by FTtrader
Key Architectural Shifts for cTrader

Collection Mutability (Positions.ToArray()): Unlike MQL5's indexed loop, cTrader manages positions as an enumerable collection. Directly iterating over Positions while calling ClosePosition() throws an exception because the collection modifies itself mid-loop. Copying the state to an array first is the safest synchronous flattening method.

Graceful Termination (Stop()): cTrader handles process termination significantly better than MT4/MT5. Calling Stop() cleanly unloads the cBot from the chart immediately after flattening the trades, preventing any orphaned ticks from triggering subsequent actions.

Broker Server Time: Server.Time.DayOfYear natively fetches the IC Markets server time, guaranteeing that the daily rollover logic executes exactly at broker midnight, entirely decoupled from your local machine's time zone.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:00 am
by FTtrader
Using ClosePositionAsync shifts the cAlgo API from a blocking synchronous operation to a non-blocking callback pattern. By dispatching all closure requests simultaneously, you eliminate the latency of waiting for broker round-trips between each trade, which is critical during flash volatility.

However, introducing asynchronous execution creates a race condition with the Stop() command. If you fire the async requests and immediately call Stop() in the OnTick() loop, the cBot will terminate before the broker processes the queue, leaving positions open.

You must decouple the shutdown sequence, utilizing System.Threading.Interlocked to track callback completions thread-safely, and only finalize the bot's termination once the network acknowledges all closures.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:00 am
by FTtrader
Here is the architectural refactor for the closure logic:

Code: Select all

private int _pendingClosures;
private bool _isShuttingDown = false; // Prevents OnTick from firing multiple shutdown sequences

// Replace the synchronous FlattenAllPositions in your OnTick breach block with this:
private void FlattenAllPositionsAsync()
{
    _isShuttingDown = true;
    var positionsToClose = Positions.ToArray();
    _pendingClosures = positionsToClose.Length;

    if (_pendingClosures == 0)
    {
        ExecuteShutdown();
        return;
    }

    foreach (var position in positionsToClose)
    {
        // Dispatches the network request immediately without blocking the thread
        ClosePositionAsync(position, OnPositionClosed);
    }
}

// The callback delegate executed by the cAlgo engine when the broker responds
private void OnPositionClosed(TradeResult result)
{
    if (!result.IsSuccessful)
    {
        Print($"Failed to close position {result.Position?.Id}: {result.Error}");
        // Note: Production systems should implement a retry policy here
    }

    // Safely decrement the counter as asynchronous callbacks return
    if (System.Threading.Interlocked.Decrement(ref _pendingClosures) == 0)
    {
        ExecuteShutdown();
    }
}

private void ExecuteShutdown()
{
    // Marshalling back to the main thread is best practice for terminal commands in cAlgo
    BeginInvokeOnMainThread(() => 
    {
        ClearLocalStorage();
        Stop();
    });
}

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:01 am
by FTtrader
Implementing the State Lock in OnTick

Because OnTick will continue firing while the asynchronous closures are in flight, you must wrap your target and breach detection blocks with a state check to prevent the bot from spamming the broker with duplicate closure requests.

Update the beginning of your OnTick method to include:

Code: Select all

protected override void OnTick()
{
    if (_isShuttingDown) return; // Halt all logic while async closures are pending
    
    // ... remaining drawdown and target math ...
}
With this pattern, the exact millisecond a floor is breached, the bot locks its state, simultaneously sprays the network with closure commands for all open exposure, dynamically waits for the broker's explicit TradeResult for every ticket, and cleanly sanitizes its memory before terminating.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:01 am
by FTtrader
To implement a robust retry policy in cTrader, you must navigate its strict threading model. While a modern C# developer might immediately reach for async/await and Task.Delay(), doing so in cAlgo is dangerous. Task.Delay resumes on a background ThreadPool thread, and calling cAlgo API methods (like ClosePositionAsync) from a background thread throws a cross-thread exception.

Re: Hitting most of the profit target in the first few days: what now?

Posted: Sat Oct 24, 2026 10:02 am
by FTtrader
Instead of a standard Polly policy, you must build a recursive callback loop that utilizes Task.Delay for the wait state, but strictly marshals the execution back to cTrader's main thread via BeginInvokeOnMainThread before re-attempting the closure.

Here is the implementation using an exponential backoff strategy:

Code: Select all

private int _pendingClosures;
private bool _isShuttingDown = false;

private void FlattenAllPositionsAsync()
{
    _isShuttingDown = true;
    var positionsToClose = Positions.ToArray();
    _pendingClosures = positionsToClose.Length;

    if (_pendingClosures == 0)
    {
        ExecuteShutdown();
        return;
    }

    foreach (var position in positionsToClose)
    {
        // Initiate the first attempt with a base 500ms delay for backoff
        ClosePositionWithRetry(position, attempt: 1, maxRetries: 4, delayMs: 500);
    }
}

private void ClosePositionWithRetry(Position position, int attempt, int maxRetries, int delayMs)
{
    ClosePositionAsync(position, result =>
    {
        if (result.IsSuccessful)
        {
            Print($"Successfully closed position {position.Id} on attempt {attempt}.");
            
            // Decrement and check for shutdown on success
            if (System.Threading.Interlocked.Decrement(ref _pendingClosures) == 0)
            {
                ExecuteShutdown();
            }
        }
        else
        {
            Print($"Closure failed for {position.Id} (Attempt {attempt}/{maxRetries}). Error: {result.Error}");

            if (attempt < maxRetries)
            {
                // Offload the wait to a background thread to prevent blocking cTrader's main thread
                System.Threading.Tasks.Task.Delay(delayMs).ContinueWith(_ =>
                {
                    // MUST marshal back to the main thread before calling the API again
                    BeginInvokeOnMainThread(() => 
                    {
                        ClosePositionWithRetry(position, attempt + 1, maxRetries, delayMs * 2);
                    });
                });
            }
            else
            {
                Print($"FATAL: Max retries exhausted for position {position.Id}. Manual intervention required.");
                
                // Still decrement on terminal failure so the bot doesn't hang indefinitely 
                // and can execute the shutdown sequence for the remaining successful closures.
                if (System.Threading.Interlocked.Decrement(ref _pendingClosures) == 0)
                {
                    ExecuteShutdown();
                }
            }
        }
    });
}

private void ExecuteShutdown()
{
    BeginInvokeOnMainThread(() => 
    {
        ClearLocalStorage();
        Stop();
    });
}