The 1% Rule Isn't Optional
Re: The 1% Rule Isn't Optional
To transition to Statistical Arbitrage, you must completely abandon the concept of individual pip-based targets.
If Leg A hits its Take Profit while Leg B remains open, you are instantly stripped of your hedge. Your delta-neutral basket becomes a directional, naked position exposed to the exact market risk you originally paid to neutralize.
Professional Stat Arb algorithms use Synthetic Basket Management. You execute both legs with a shared Unique Identifier (UID) and track their combined fiat Net Profit. When the aggregate PnL hits your Kelly expected value, you liquidate the entire basket simultaneously.
Here is the architectural rewrite to build a unified Basket Manager in cTrader.
If Leg A hits its Take Profit while Leg B remains open, you are instantly stripped of your hedge. Your delta-neutral basket becomes a directional, naked position exposed to the exact market risk you originally paid to neutralize.
Professional Stat Arb algorithms use Synthetic Basket Management. You execute both legs with a shared Unique Identifier (UID) and track their combined fiat Net Profit. When the aggregate PnL hits your Kelly expected value, you liquidate the entire basket simultaneously.
Here is the architectural rewrite to build a unified Basket Manager in cTrader.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
1. Master Robot: The Basket Manager
We must add a dictionary to the Master Robot to track the exact fiat targets for each unique basket in memory. We also add a ManageBaskets() method that runs on every tick to sum the PnL of grouped positions.
Add this code to your InstKellyMultiScalper class:
Note: You must also add ManageBaskets(); to the very top of your master OnTick() loop so it evaluates the portfolio's aggregate PnL on every price change.
We must add a dictionary to the Master Robot to track the exact fiat targets for each unique basket in memory. We also add a ManageBaskets() method that runs on every tick to sum the PnL of grouped positions.
Add this code to your InstKellyMultiScalper class:
Code: Select all
// =========================================================================
// BASKET STATE TRACKING (Master Robot)
// =========================================================================
// Stores the fiat Target ($) and Stop Loss ($) for each unique basket ID
private Dictionary<string, (double TargetPnL, double StopPnL)> _activeBaskets = new Dictionary<string, (double, double)>();
public void RegisterBasket(string basketId, double targetPnL, double stopPnL)
{
_activeBaskets[basketId] = (targetPnL, stopPnL);
}
private void ManageBaskets()
{
// 1. Gather all open positions belonging to our algorithm
var allPositions = Positions.FindAll("InstKellyMulti");
// 2. Group them by their unique Basket ID (stored in the Comment field)
var baskets = allPositions
.Where(p => !string.IsNullOrEmpty(p.Comment) && p.Comment.StartsWith("Arb_"))
.GroupBy(p => p.Comment);
foreach (var basketGroup in baskets)
{
string basketId = basketGroup.Key;
// Calculate the aggregate Net PnL (Gross Profit + Swaps + Commissions)
double currentNetPnL = basketGroup.Sum(p => p.NetProfit);
if (_activeBaskets.TryGetValue(basketId, out var limits))
{
// 3. Evaluate combined fiat PnL against our dynamic Kelly targets
if (currentNetPnL >= limits.TargetPnL || currentNetPnL <= limits.StopPnL)
{
string exitReason = currentNetPnL >= limits.TargetPnL ? "Target Reached" : "Stop Hit";
Print($"[Basket Exit] {basketId} Liquidated. Reason: {exitReason}. Net PnL: ${Math.Round(currentNetPnL, 2)}");
// 4. Liquidate all legs simultaneously
foreach (var pos in basketGroup)
{
ClosePosition(pos);
}
// Clean up memory
_activeBaskets.Remove(basketId);
}
}
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
2. Symbol Engine: Basket Execution
Now we rewrite the execution logic inside SymbolEngine.Evaluate(). We generate a unique basketId, calculate our fiat risk amount, and pass it to the Master Robot.
Crucially, we still assign a physical Stop Loss to the broker (catastrophicStopPips), placed 50% wider than our intended synthetic stop. This acts as an infrastructure circuit-breaker—if your VPS loses internet connection, the exchange will still close the trades before they blow the account.
Now we rewrite the execution logic inside SymbolEngine.Evaluate(). We generate a unique basketId, calculate our fiat risk amount, and pass it to the Master Robot.
Crucially, we still assign a physical Stop Loss to the broker (catastrophicStopPips), placed 50% wider than our intended synthetic stop. This acts as an infrastructure circuit-breaker—if your VPS loses internet connection, the exchange will still close the trades before they blow the account.
Code: Select all
// =================================================================
// BASKET EXECUTION LOGIC (Inside SymbolEngine.Evaluate)
// =================================================================
string hedgeSymbolName = _algo.FindOptimalHedge(_symbol.Name, out double correlation);
if (hedgeSymbolName != null && correlation <= _algo.InpMinNegativeCorr)
{
// 1. Calculate Leg A Volume & Target Risk
double stopDistPipsA = (_atr.Result.Last(0) * _algo.InpAtrMultiplier) / _symbol.PipSize;
double volumeA = CalculatePositionVolume(stopDistPipsA);
if (volumeA == 0) return;
// 2. Calculate Leg B Volatility Parity Volume
Symbol hedgeSymbol = _algo.Symbols.GetSymbol(hedgeSymbolName);
var hedgeBars = _algo.MarketData.GetBars(_algo.TimeFrame, hedgeSymbolName);
var hedgeAtr = _algo.Indicators.AverageTrueRange(hedgeBars, _algo.InpAtrPeriod, MovingAverageType.Simple);
double atrA = _atr.Result.Last(0) / _symbol.PipSize;
double atrB = hedgeAtr.Result.Last(0) / hedgeSymbol.PipSize;
double rawVolumeB = volumeA * ((atrA * _symbol.PipValue) / (atrB * hedgeSymbol.PipValue));
double volumeB = hedgeSymbol.NormalizeVolumeInUnits(rawVolumeB, RoundingMode.Down);
// 3. Generate Unique Basket ID and Calculate Fiat Targets
// We use Server.Time.Ticks to ensure absolute uniqueness
string basketId = $"Arb_{_symbol.Name}_{_algo.Server.Time.Ticks}";
double riskAmountFiat = _algo.Account.Equity * _cachedAppliedRisk;
double targetPnLFiat = riskAmountFiat * _cachedDynRR;
double stopPnLFiat = -riskAmountFiat;
_algo.RegisterBasket(basketId, targetPnLFiat, stopPnLFiat);
// 4. Catastrophic VPS Failure Stops (Placed 50% wider than synthetic stop)
double catastrophicStopA = stopDistPipsA * 1.5;
double catastrophicStopB = (_algo.InpAtrMultiplier * atrB) * 1.5;
// 5. Execute Linked Basket (Both use _label to track them, and basketId to group them)
_algo.ExecuteMarketOrder(triggerDirection, _symbol.Name, volumeA, _label, catastrophicStopA, null, basketId, false);
_algo.ExecuteMarketOrder(triggerDirection, hedgeSymbolName, volumeB, _label, catastrophicStopB, null, basketId, false);
_algo.Print($"[Basket Generated] ID: {basketId} | Target: ${Math.Round(targetPnLFiat, 2)} | Hard Risk: ${Math.Round(riskAmountFiat, 2)}");
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
The Power of Aggregate Management
By using LINQ basketGroup.Sum(p => p.NetProfit), you automatically factor in broker commissions and overnight swap fees for both legs. If Leg A is up +$100, but Leg B is down -$40 and has accumulated -$10 in overnight swap friction, your true Net PnL is +$50.
Static limit targets set on the broker side are entirely blind to swap decay and commission friction. Managing the aggregate fiat PnL synthetically is the only way to guarantee your strategy perfectly maps to the rolling Kelly expectancy curve we built in step one.
By using LINQ basketGroup.Sum(p => p.NetProfit), you automatically factor in broker commissions and overnight swap fees for both legs. If Leg A is up +$100, but Leg B is down -$40 and has accumulated -$10 in overnight swap friction, your true Net PnL is +$50.
Static limit targets set on the broker side are entirely blind to swap decay and commission friction. Managing the aggregate fiat PnL synthetically is the only way to guarantee your strategy perfectly maps to the rolling Kelly expectancy curve we built in step one.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
To eliminate client-side slippage, we must address the "Ping Penalty."
When you use the synchronous ClosePosition(pos) command in a foreach loop, your cBot halts execution on the first leg, waits for the network packet to travel to the broker, waits for the exchange matching engine, and waits for the confirmation packet to return (e.g., 40ms) before it transmits the request to close the second leg. During that 40ms delay, the price of Leg B can move against you.
By utilizing C# Task and async/await, we can dispatch the network packets to your network interface card sequentially in microseconds. While you cannot physically guarantee the exchange matching engine processes them in the exact same millisecond due to order book queuing, this asynchronous architecture entirely eliminates the client-side network latency gap.
Here is how you implement concurrent multi-leg liquidation in cTrader.
When you use the synchronous ClosePosition(pos) command in a foreach loop, your cBot halts execution on the first leg, waits for the network packet to travel to the broker, waits for the exchange matching engine, and waits for the confirmation packet to return (e.g., 40ms) before it transmits the request to close the second leg. During that 40ms delay, the price of Leg B can move against you.
By utilizing C# Task and async/await, we can dispatch the network packets to your network interface card sequentially in microseconds. While you cannot physically guarantee the exchange matching engine processes them in the exact same millisecond due to order book queuing, this asynchronous architecture entirely eliminates the client-side network latency gap.
Here is how you implement concurrent multi-leg liquidation in cTrader.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
1. The Async Liquidation Method
Add this asynchronous helper method to your InstKellyMultiScalper class. It collects the API tasks and awaits them concurrently.
Add this asynchronous helper method to your InstKellyMultiScalper class. It collects the API tasks and awaits them concurrently.
Code: Select all
// =========================================================================
// CONCURRENT ASYNC EXECUTION
// =========================================================================
private async Task LiquidateBasketConcurrentAsync(List<Position> basket, string basketId)
{
// 1. Initialize a list to hold our in-flight network tasks
var closeTasks = new List<Task<TradeResult>>();
// 2. Dispatch all requests to the broker simultaneously without blocking
foreach (var pos in basket)
{
closeTasks.Add(ClosePositionAsync(pos));
}
// 3. Yield the thread and await all broker confirmations concurrently
TradeResult[] results = await Task.WhenAll(closeTasks);
// 4. Post-execution verification
int successCount = results.Count(r => r.IsSuccessful);
if (successCount == basket.Count)
{
Print($"[Basket Closed] {basketId} - All {successCount} legs filled concurrently.");
}
else
{
// Handle partial liquidations (e.g., broker rejected one leg due to liquidity)
int failCount = basket.Count - successCount;
Print($"[CRITICAL WARNING] {basketId} - {failCount} legs failed to close. Portfolio is directionally exposed.");
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
2. Bridging Synchronous OnTick to Async Tasks
Because cTrader's OnTick() method signature is strictly synchronous (void), you cannot natively await an asynchronous method inside it. Instead, you invoke the async method as a "fire-and-forget" operation using the C# discard operator (_). This instructs the .NET CLR to spin up the Task on a background state machine while allowing OnTick() to immediately resume parsing market data.
Update the execution block inside your ManageBaskets() method:
Because cTrader's OnTick() method signature is strictly synchronous (void), you cannot natively await an asynchronous method inside it. Instead, you invoke the async method as a "fire-and-forget" operation using the C# discard operator (_). This instructs the .NET CLR to spin up the Task on a background state machine while allowing OnTick() to immediately resume parsing market data.
Update the execution block inside your ManageBaskets() method:
Code: Select all
private void ManageBaskets()
{
var allPositions = Positions.FindAll("InstKellyMulti");
var baskets = allPositions
.Where(p => !string.IsNullOrEmpty(p.Comment) && p.Comment.StartsWith("Arb_"))
.GroupBy(p => p.Comment);
foreach (var basketGroup in baskets)
{
string basketId = basketGroup.Key;
double currentNetPnL = basketGroup.Sum(p => p.NetProfit);
if (_activeBaskets.TryGetValue(basketId, out var limits))
{
if (currentNetPnL >= limits.TargetPnL || currentNetPnL <= limits.StopPnL)
{
string exitReason = currentNetPnL >= limits.TargetPnL ? "Target Reached" : "Stop Hit";
Print($"[Basket Exit Initiated] {basketId}. Reason: {exitReason}. Net PnL: ${Math.Round(currentNetPnL, 2)}");
// Evaluate the group to a static list to prevent lazy-loading collection modification exceptions
var basketList = basketGroup.ToList();
// Fire and forget the concurrent liquidation
_ = LiquidateBasketConcurrentAsync(basketList, basketId);
// Instantly remove from tracking memory so OnTick doesn't trigger it again on the next millisecond tick
_activeBaskets.Remove(basketId);
}
}
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
The Institutional Edge: Handling Rejections
In standard C# enterprise architecture, a failed database write throws an exception that you catch. In algorithmic trading, if Leg A closes successfully but Leg B is rejected by the broker (e.g., TradeResult.IsSuccessful == false due to a sudden lack of order book liquidity or a "requote"), your algorithm does not crash. It simply leaves Leg B running indefinitely.
Because we used Task.WhenAll, the results array in the async method captures the exact broker response for every leg. If a leg fails to close, you are now holding a naked, unhedged position.
In standard C# enterprise architecture, a failed database write throws an exception that you catch. In algorithmic trading, if Leg A closes successfully but Leg B is rejected by the broker (e.g., TradeResult.IsSuccessful == false due to a sudden lack of order book liquidity or a "requote"), your algorithm does not crash. It simply leaves Leg B running indefinitely.
Because we used Task.WhenAll, the results array in the async method captures the exact broker response for every leg. If a leg fails to close, you are now holding a naked, unhedged position.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
In algorithmic trading, when a broker rejects an order to close an existing position, attempting a "Rescue Hedge" is the fastest way to stabilize the portfolio.
Instead of searching for a correlated pair (which carries tracking error risk), the most mathematically sound approach is to execute an exact opposing market order on the same symbol. If Leg B is stuck as Long 1.0 Lot GBPUSD, you instantly execute a Short 1.0 Lot GBPUSD. This perfectly neutralizes your delta exposure and permanently locks your floating PnL at its current value while you investigate the API error.
Here is how to implement the Rescue Hedge pattern natively within your asynchronous liquidation method.
Instead of searching for a correlated pair (which carries tracking error risk), the most mathematically sound approach is to execute an exact opposing market order on the same symbol. If Leg B is stuck as Long 1.0 Lot GBPUSD, you instantly execute a Short 1.0 Lot GBPUSD. This perfectly neutralizes your delta exposure and permanently locks your floating PnL at its current value while you investigate the API error.
Here is how to implement the Rescue Hedge pattern natively within your asynchronous liquidation method.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: The 1% Rule Isn't Optional
The "Rescue Protocol" Implementation
We update LiquidateBasketConcurrentAsync to pair the broker's response array with your original basket list. If any leg returns IsSuccessful == false, the engine instantly reverses the TradeType and fires a counter-order.
We update LiquidateBasketConcurrentAsync to pair the broker's response array with your original basket list. If any leg returns IsSuccessful == false, the engine instantly reverses the TradeType and fires a counter-order.
Code: Select all
// =========================================================================
// CONCURRENT ASYNC EXECUTION & RESCUE PROTOCOL
// =========================================================================
private async Task LiquidateBasketConcurrentAsync(List<Position> basket, string basketId)
{
var closeTasks = new List<Task<TradeResult>>();
// 1. Dispatch close requests concurrently
foreach (var pos in basket)
{
closeTasks.Add(ClosePositionAsync(pos));
}
// 2. Await broker execution
TradeResult[] results = await Task.WhenAll(closeTasks);
// 3. Verify execution integrity
var failedIndexes = new List<int>();
for (int i = 0; i < results.Length; i++)
{
if (!results[i].IsSuccessful) failedIndexes.Add(i);
}
if (failedIndexes.Count == 0)
{
Print($"[Basket Closed] {basketId} - All legs filled concurrently.");
return; // Clean exit
}
// =====================================================================
// 4. INITIATE RESCUE PROTOCOL
// =====================================================================
Print($"[CRITICAL WARNING] {basketId} - {failedIndexes.Count} legs failed to close. Initiating Rescue Hedge.");
var hedgeTasks = new List<Task<TradeResult>>();
foreach (int index in failedIndexes)
{
// Identify the exact orphaned position
Position orphanedPos = basket[index];
TradeResult errorResult = results[index];
Print($"[API Error] Failed to close {orphanedPos.SymbolName}. Broker response: {errorResult.Error}");
// Invert the direction to achieve zero-delta parity
TradeType counterDirection = orphanedPos.TradeType == TradeType.Buy ? TradeType.Sell : TradeType.Buy;
Print($"[Rescue Protocol] Hedging orphaned {orphanedPos.TradeType} {orphanedPos.SymbolName} | Volume: {orphanedPos.VolumeInUnits}");
// Dispatch the rescue hedge concurrently
hedgeTasks.Add(ExecuteMarketOrderAsync(
counterDirection,
orphanedPos.SymbolName,
orphanedPos.VolumeInUnits,
"RescueHedge",
0,
0,
$"Rescue_{basketId}",
false
));
}
// 5. Await emergency stabilization
TradeResult[] hedgeResults = await Task.WhenAll(hedgeTasks);
// 6. Final state evaluation
int successfulHedges = hedgeResults.Count(r => r.IsSuccessful);
if (successfulHedges == failedIndexes.Count)
{
Print($"[System Stabilized] Orphaned legs successfully locked. Delta is perfectly neutral.");
}
else
{
Print($"[CATASTROPHIC ERROR] Rescue hedge failed. Portfolio is heavily exposed. Manual intervention required!");
// Hook external webhook/SMS alerts here
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.