Page 5 of 7
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:01 pm
by PTScalper
2. Update the Symbol Engine (Execution Gate)
Inside the SymbolEngine class, modify the Evaluate() method. Place the global capacity check after the moving average crossover logic, but before you calculate volume or send the order to the exchange.
Code: Select all
public void Evaluate()
{
if (_algo.SystemHalted) return;
ManageOpenPositions();
var openPositions = _algo.Positions.FindAll(_label, _symbol.Name);
if (openPositions.Length == 0)
{
_tp1Hit = false;
bool isLongTrigger = _fastSma.Result.Last(1) <= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) > _slowSma.Result.Last(0);
bool isShortTrigger = _fastSma.Result.Last(1) >= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) < _slowSma.Result.Last(0);
if (isLongTrigger || isShortTrigger)
{
// =================================================================
// GLOBAL PORTFOLIO LIMIT CHECK
// =================================================================
if (_algo.TotalOpenPositions >= _algo.InpMaxGlobalPositions)
{
// The engine detects an edge, but the master portfolio
// is at maximum capacity. Abort execution.
return;
}
double stopDistPips = (_atr.Result.Last(0) * _algo.InpAtrMultiplier) / _symbol.PipSize;
double volume = CalculatePositionVolume(stopDistPips);
if (volume > 0)
{
var tradeType = isLongTrigger ? TradeType.Buy : TradeType.Sell;
ExecuteTrade(tradeType, volume, stopDistPips);
}
}
}
}
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:01 pm
by PTScalper
Advanced: Currency-Specific Exposure Tracking
While capping total trades at 2 achieves your goal if you are only trading EURUSD and GBPUSD, it becomes insufficient if you scale the bot to 15 currency pairs. You might end up with EURUSD and USDJPY open (neutralizing USD exposure), or EURUSD and GBPUSD open (doubling USD short exposure).
To build a true institutional exposure matrix, you would replace TotalOpenPositions with a LINQ query that parses the symbol strings for specific currencies:
Code: Select all
// Inside the Master Robot
public double GetNetCurrencyExposure(string currency)
{
double netExposure = 0;
var allPositions = Positions.FindAll("InstKellyMulti");
foreach (var pos in allPositions)
{
// Example: If Long EURUSD, we are Short USD
if (pos.SymbolName.EndsWith(currency))
netExposure += pos.TradeType == TradeType.Buy ? -pos.VolumeInUnits : pos.VolumeInUnits;
// Example: If Long USDJPY, we are Long USD
else if (pos.SymbolName.StartsWith(currency))
netExposure += pos.TradeType == TradeType.Buy ? pos.VolumeInUnits : -pos.VolumeInUnits;
}
return netExposure;
}
The engine would then query _algo.GetNetCurrencyExposure("USD") and block the trade if taking the new position would push your net aggregate USD volume past a defined threshold.
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:02 pm
by PTScalper
To build a dynamic correlation matrix, you must calculate the Pearson Correlation Coefficient between the two assets at the exact moment your algorithm generates an entry signal.
Because you are scalping 1-minute or 5-minute price action, calculating correlation on raw closing prices is mathematically flawed (financial time series are non-stationary). You must calculate the correlation of their percentage returns over a rolling window.
Here is the architectural update to implement the Correlation Matrix into your cTrader engine.
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:02 pm
by PTScalper
1. Update the Master Robot (Inputs & Pearson Math)
Add the correlation constraints to your InstKellyMultiScalper parameters, and include a highly optimized, single-pass Pearson calculation method.
Code: Select all
// =========================================================================
// CORRELATION LIMIT PARAMETERS
// =========================================================================
[Parameter("Max Allowed Correlation", DefaultValue = 0.75, MinValue = 0.1, MaxValue = 1.0, Group = "Portfolio Risk Limits",
Tooltip = "0.75 means reject if 75% correlated.")]
public double InpMaxCorrelation { get; set; }
[Parameter("Correlation Lookback (Bars)", DefaultValue = 50, MinValue = 10, Group = "Portfolio Risk Limits")]
public int InpCorrLookback { get; set; }
// =========================================================================
// PEARSON MATH UTILITY (Place inside InstKellyMultiScalper)
// =========================================================================
public double GetReturnCorrelation(string symbolA, string symbolB, int periods)
{
var barsA = MarketData.GetBars(TimeFrame, symbolA).ClosePrices;
var barsB = MarketData.GetBars(TimeFrame, symbolB).ClosePrices;
// Ensure we have enough data
if (barsA.Count <= periods || barsB.Count <= periods) return 0;
double sumX = 0, sumY = 0, sumX2 = 0, sumY2 = 0, sumXY = 0;
int n = periods;
// Loop through the requested window (skipping the active unclosed bar at index 0)
for (int i = 1; i <= n; i++)
{
// Calculate percentage returns: (Current - Previous) / Previous
double returnA = (barsA.Last(i) - barsA.Last(i + 1)) / barsA.Last(i + 1);
double returnB = (barsB.Last(i) - barsB.Last(i + 1)) / barsB.Last(i + 1);
sumX += returnA;
sumY += returnB;
sumX2 += returnA * returnA;
sumY2 += returnB * returnB;
sumXY += returnA * returnB;
}
double numerator = (n * sumXY) - (sumX * sumY);
double denominator = Math.Sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
return denominator == 0 ? 0 : numerator / denominator;
}
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:03 pm
by PTScalper
2. Update the Symbol Engine (The Directional Filter)
In your SymbolEngine class, before executing the trade, you must iterate over all currently open positions in the portfolio.
The logic requires a Directional Check. If you are buying EURUSD, and you are already long GBPUSD, a high positive correlation ($+0.85$) is dangerous. However, if you are buying EURUSD and you are already short USDCHF, a high negative correlation ($-0.90$) is equally dangerous, because buying EUR and shorting USDCHF is fundamentally the same directional USD exposure.
Update the Evaluate() method in the SymbolEngine class:
Code: Select all
public void Evaluate()
{
if (_algo.SystemHalted) return;
ManageOpenPositions();
var openPositions = _algo.Positions.FindAll("InstKellyMulti", _symbol.Name);
if (openPositions.Length == 0)
{
_tp1Hit = false;
bool isLongTrigger = _fastSma.Result.Last(1) <= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) > _slowSma.Result.Last(0);
bool isShortTrigger = _fastSma.Result.Last(1) >= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) < _slowSma.Result.Last(0);
if (isLongTrigger || isShortTrigger)
{
var triggerDirection = isLongTrigger ? TradeType.Buy : TradeType.Sell;
// =================================================================
// DYNAMIC CORRELATION MATRIX CHECK
// =================================================================
var globalPositions = _algo.Positions.FindAll("InstKellyMulti");
foreach (var openPos in globalPositions)
{
// Calculate dynamic Pearson correlation between the trigger symbol and the open position symbol
double correlation = _algo.GetReturnCorrelation(_symbol.Name, openPos.SymbolName, _algo.InpCorrLookback);
bool sameDirection = triggerDirection == openPos.TradeType;
// Risk Case 1: Strong Positive Correlation AND Same Direction (e.g., Long EURUSD, Long GBPUSD)
if (correlation > _algo.InpMaxCorrelation && sameDirection)
{
_algo.Print($"[Rejected] {_symbol.Name} {triggerDirection} is {Math.Round(correlation*100, 1)}% positively correlated with open {openPos.SymbolName} {openPos.TradeType}.");
return;
}
// Risk Case 2: Strong Negative Correlation AND Opposite Direction (e.g., Long EURUSD, Short USDCHF)
if (correlation < -_algo.InpMaxCorrelation && !sameDirection)
{
_algo.Print($"[Rejected] {_symbol.Name} {triggerDirection} is {Math.Round(correlation*100, 1)}% negatively correlated with open {openPos.SymbolName} {openPos.TradeType}.");
return;
}
}
// Passed all correlation checks. Proceed to sizing and execution.
double stopDistPips = (_atr.Result.Last(0) * _algo.InpAtrMultiplier) / _symbol.PipSize;
double volume = CalculatePositionVolume(stopDistPips);
if (volume > 0)
{
ExecuteTrade(triggerDirection, volume, stopDistPips);
}
}
}
}
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:04 pm
by PTScalper
Architectural Efficiencies
On-Demand Processing: The Pearson math uses a standard for loop instead of LINQ for speed. More importantly, it only fires when a trigger condition is met, saving massive CPU cycles compared to continuously updating a background correlation grid on every OnTick event.
Cross-Timeframe Portability: By passing TimeFrame dynamically into MarketData.GetBars(TimeFrame, symbolA), the correlation window perfectly matches your scalping horizon whether you attach the bot to a 1-minute or 15-minute chart.
Self-Exclusion Safe: The openPositions.Length == 0 check ensures that the engine only calculates correlation against other symbols' open trades, preventing the algorithm from accidentally blocking itself.
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:05 pm
by PTScalper
To pivot from directional scalping to Statistical Arbitrage (Stat Arb), you fundamentally change the engine's objective. Instead of rejecting a trade because of correlation, you actively hunt the correlation matrix for a "partner" asset to absorb your systemic risk (e.g., delta-neutralizing your USD exposure).
If your algorithm signals a Long on EURUSD, and you find a highly negatively correlated asset (e.g., USDCHF at $r = -0.92$), you execute a Long/Long basket. Because they move inversely, going Long on both neutralizes the USD denominator. Your trade is no longer a bet on the US Dollar; it is a relative value bet that the Euro will outperform the Swiss Franc.
To execute this, we must introduce Volatility Parity. You cannot simply buy 1 Lot of A and 1 Lot of B. If USDCHF is currently twice as volatile as EURUSD, a 1:1 lot size leaves you severely exposed to CHF.
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:05 pm
by PTScalper
1. The Volatility Parity Equation
To achieve a true delta-neutral hedge, the fiat value of Leg B's daily movement must perfectly match Leg A's movement.
2. The Master Robot: Hedging Scanner
Add the hedging parameters to InstKellyMultiScalper and a method that scans the global array for the optimal negative correlation partner.
Code: Select all
[Parameter("Min Negative Correlation", DefaultValue = -0.80, MaxValue = -0.50, Group = "Stat Arb Settings")]
public double InpMinNegativeCorr { get; set; }
// Scans the active symbol list to find the strongest inverse pair
public string FindOptimalHedge(string primarySymbol, out double correlation)
{
string bestHedge = null;
double lowestCorr = 1.0;
string[] allSymbols = InpSymbols.Split(',').Select(s => s.Trim()).ToArray();
foreach (string sym in allSymbols)
{
if (sym == primarySymbol) continue;
double currentCorr = GetReturnCorrelation(primarySymbol, sym, InpCorrLookback);
if (currentCorr < lowestCorr)
{
lowestCorr = currentCorr;
bestHedge = sym;
}
}
correlation = lowestCorr;
return bestHedge;
}
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:06 pm
by PTScalper
3. The Symbol Engine: Basket Execution
We rewrite the Evaluate() method inside the SymbolEngine. When the SMA crossover fires, it pauses, scans for a hedge, calculates Volatility Parity, and fires a synchronized basket.
Code: Select all
public void Evaluate()
{
if (_algo.SystemHalted) return;
ManageOpenPositions(); // Manages Leg A partials
var openPositions = _algo.Positions.FindAll("InstKellyMulti", _symbol.Name);
if (openPositions.Length == 0)
{
_tp1Hit = false;
bool isLongTrigger = _fastSma.Result.Last(1) <= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) > _slowSma.Result.Last(0);
bool isShortTrigger = _fastSma.Result.Last(1) >= _slowSma.Result.Last(1) && _fastSma.Result.Last(0) < _slowSma.Result.Last(0);
if (isLongTrigger || isShortTrigger)
{
var triggerDirection = isLongTrigger ? TradeType.Buy : TradeType.Sell;
// =================================================================
// 1. SEEK STATISTICAL ARBITRAGE PARTNER
// =================================================================
string hedgeSymbolName = _algo.FindOptimalHedge(_symbol.Name, out double correlation);
if (hedgeSymbolName != null && correlation <= _algo.InpMinNegativeCorr)
{
_algo.Print($"[Stat Arb] {_symbol.Name} paired with {hedgeSymbolName}. Correlation: {Math.Round(correlation, 2)}");
// =================================================================
// 2. CALCULATE LEG A (Primary Kelly Volume)
// =================================================================
double stopDistPipsA = (_atr.Result.Last(0) * _algo.InpAtrMultiplier) / _symbol.PipSize;
double volumeA = CalculatePositionVolume(stopDistPipsA);
if (volumeA == 0) return;
// =================================================================
// 3. 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; // Convert to pips
double atrB = hedgeAtr.Result.Last(0) / hedgeSymbol.PipSize;
// Parity Math: Match the fiat dollar variance of both assets
double rawVolumeB = volumeA * ((atrA * _symbol.PipValue) / (atrB * hedgeSymbol.PipValue));
double volumeB = hedgeSymbol.NormalizeVolumeInUnits(rawVolumeB, RoundingMode.Down);
// =================================================================
// 4. BASKET EXECUTION
// =================================================================
// Because they are negatively correlated, going the SAME direction makes us Delta-Neutral.
ExecuteTrade(triggerDirection, volumeA, stopDistPipsA); // Leg A Execution
// Leg B Execution (We use a different label to manage it as a hedge)
double stopDistPipsB = (_algo.InpAtrMultiplier * atrB);
_algo.ExecuteMarketOrder(triggerDirection, hedgeSymbolName, volumeB, "StatArb_Hedge", stopDistPipsB, null);
}
}
}
}
Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 2:06 pm
by PTScalper
The Trade Management Reality
When executing a delta-neutral basket, managing the exit becomes the primary engineering challenge.
Because Leg B was executed with the label "StatArb_Hedge", your existing ManageOpenPositions() method will ignore it. This is intentional. In pairs trading, you usually do not scale out of legs independently based on static TP targets. If Leg A hits TP1 and you scale out half, but Leg B is left intact, you instantly lose delta-neutrality and are suddenly directionally exposed to the market on the remaining Leg B volume.