NetProfit vs. GrossProfit: The snippet specifically calls .NetProfit rather than .GrossProfit. For short-term price action scalping, gross profit is a dangerous metric because it represents pure price movement. NetProfit automatically deducts the round-turn commissions and overnight swap fees, ensuring your calculation represents true account equity impact.
LINQ Optimization: If you prefer cleaner syntax and your system supports it, you can collapse the loops using LINQ:
Note: While LINQ is cleaner, explicit foreach loops are slightly faster in high-frequency OnTick execution environments because they avoid the overhead of delegate allocation and closure generation.
Handling Grid / Martingale Systems: Because this aggregates everything with the matching label, this exact function works flawlessly if your strategy scales in (adding multiple new positions to the same trade idea) rather than just scaling out. It will sum the entire portfolio of trades associated with that specific LTID.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
To execute a fiat-based mass closure, you must calculate the live aggregate PnL on every tick and, crucially, implement a state-lock to prevent the bot from spamming the broker with redundant close requests during the network latency window.
When a logical trade crosses your target profit, you fire asynchronous close requests for all remaining active fragments simultaneously.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class FiatTargetManager : Robot
{
[Parameter("Target Net PnL (Fiat)", Group = "Trade Management", DefaultValue = 50.0)]
public double TargetNetPnL { get; set; }
// State lock to prevent micro-tick spam while waiting for the broker to close positions
private HashSet<string> _closingLtids = new HashSet<string>();
protected override void OnStart()
{
Positions.Closed += OnPositionClosed;
}
protected override void OnTick()
{
// 1. Get all unique Logical Trade IDs (Labels) currently active in the market
var activeLtids = Positions.Select(p => p.Label).Distinct();
foreach (var ltid in activeLtids)
{
// Skip if this LTID is already in the process of being closed
if (_closingLtids.Contains(ltid)) continue;
// 2. Calculate the true Net PnL (Realized + Unrealized)
double currentNetPnL = CalculateLogicalTradeNetPnL(ltid);
// 3. Trigger mass closure if the target is met
if (currentNetPnL >= TargetNetPnL)
{
ExecuteMassClosure(ltid, currentNetPnL);
}
}
}
private void ExecuteMassClosure(string ltid, double triggeredPnL)
{
// 1. Lock the LTID immediately so subsequent ticks ignore it
_closingLtids.Add(ltid);
Print($"[Target Reached] LTID: {ltid} hit {triggeredPnL:C2}. Initiating mass closure.");
// 2. Fetch all remaining open fragments for this LTID
var fragmentsToClose = Positions.FindAll(ltid);
// 3. Fire asynchronous close requests for all fragments concurrently
foreach (var position in fragmentsToClose)
{
ClosePositionAsync(position, OnFragmentCloseComplete);
}
}
private double CalculateLogicalTradeNetPnL(string ltid)
{
// Fast LINQ aggregation of realized history and unrealized active positions
return History.FindAll(ltid).Sum(t => t.NetProfit) +
Positions.FindAll(ltid).Sum(p => p.NetProfit);
}
private void OnFragmentCloseComplete(TradeResult result)
{
if (!result.IsSuccessful && result.Position != null)
{
// If the broker rejects a close request (e.g., off-quotes), unlock the LTID
// so the bot re-evaluates and tries to close it again on the next tick.
_closingLtids.Remove(result.Position.Label);
Print($"[Error] Failed to close fragment {result.Position.Id}. Re-arming. Error: {result.Error}");
}
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
string closedLabel = args.Position.Label;
// Once the final fragment of the LTID is successfully closed, clear the state lock
if (Positions.FindAll(closedLabel).Length == 0)
{
_closingLtids.Remove(closedLabel);
Print($"[Mass Closure Complete] Logical Trade {closedLabel} fully liquidated.");
}
}
}
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
LINQ Grouping (Select(p => p.Label).Distinct()): If a trade has been scaled out and currently has 4 open child positions, a standard foreach loop over Positions would calculate the heavy PnL math 4 separate times per tick. Extracting the distinct labels first ensures the bot calculates the aggregate PnL only once per logical trade, significantly reducing CPU load during high-volatility tick spikes.
The Execution State Lock (_closingLtids): It takes roughly 50 to 200 milliseconds for a trade to close on a live server. During that window, cTrader will process dozens of new ticks. If you do not lock the string label in the _closingLtids hashset before calling ClosePositionAsync, the bot will flood the broker's API with redundant close commands for a position that is already processing a closure, often triggering temporary IP bans from the broker.
Concurrent Batching: The foreach loop inside ExecuteMassClosure utilizes ClosePositionAsync. It does not wait for Fragment A to close before asking the broker to close Fragment B. It fires all requests in a single millisecond batch, allowing the broker's matching engine to execute them parallelly, minimizing slippage drag.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Risk Note on Fiat Slippage: Market orders guarantee execution, not price. If you trigger the mass closure exactly at $50.00, the actual realized net profit will likely settle slightly lower (e.g., $49.60) because you are aggressively crossing the spread and exiting at the Bid/Ask during live market movement.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
To implement a fiat-based trailing stop, you must introduce a High-Water Mark memory system. The bot needs to remember the absolute highest net profit a logical trade has achieved since it crossed the activation threshold, and constantly evaluate the current profit against that peak minus your trailing distance.
If the peak profit was $100 and your trailing distance is $10, the bot dynamically sets the hidden tripwire at $90. If the profit drops to $90, it liquidates the basket.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Dictionary State Tracking (Dictionary<string, double>): A HashSet only remembers if a state exists; a Dictionary remembers what the state is. _highWaterMarks maps your unique logical trade label directly to its peak fiat value.
Persistent Activation: The condition (currentNetPnL >= ActivationProfit || _highWaterMarks.ContainsKey(ltid)) is vital. If a trade hits $52 (activating the dictionary), but then drops to $45 on the next tick, it must remain actively trailed because the tripwire is locked at $42 ($52 peak minus $10 distance). The || _highWaterMarks.ContainsKey ensures the trail does not deactivate just because profit dips temporarily below the initial activation threshold.
Gap Logic / Slippage Reality: The OnTick method fires sequentially with price updates. If the market is moving smoothly, currentNetPnL will hit exactly $40.00 and trigger. However, if a massive news candle gaps price over your tripwire, currentNetPnL might jump instantly from $42.00 down to $35.00. The logic currentNetPnL <= dynamicTripwire ensures the liquidation still fires, executing at $35.00 to prevent further loss.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
To project dynamic tracking data directly onto your chart, you should use cTrader's Chart.DrawStaticText method rather than standard chart text. Static text pins the data to the screen coordinates (like a Heads-Up Display or HUD), meaning it stays perfectly visible regardless of how far you zoom in, zoom out, or scroll back in time.
Because multiple logical trades (LTIDs) might be active at once, the most robust architecture is to build a StringBuilder that loops through your active dictionary and refreshes the HUD on every tick.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.