Open the Automate tab, click New cBot, and name it ChronosSweepPyramidBot.
Replace the entire template with the code below and click Build.
Code: Select all
using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class ChronosSweepPyramidBot : Robot
{
#region 1. Liquidity Pool & Sweep Parameters
[Parameter("Bot Instance Label", DefaultValue = "Chronos_HMP", Group = "0. Engine Identity")]
public string BotLabel { get; set; }
[Parameter("Swing Pivot Strength (Bars)", DefaultValue = 5, MinValue = 2, MaxValue = 20, Group = "1. Liquidity Pool Geometry")]
public int SwingStrength { get; set; }
[Parameter("Max Pool Age (Bars)", DefaultValue = 120, MinValue = 20, MaxValue = 500, Group = "1. Liquidity Pool Geometry")]
public int MaxPoolAgeBars { get; set; }
[Parameter("Min Sweep Depth (ATR Mult)", DefaultValue = 0.10, MinValue = 0.01, MaxValue = 1.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
public double MinSweepAtr { get; set; }
[Parameter("Max Sweep Depth (ATR Mult)", DefaultValue = 1.30, MinValue = 0.30, MaxValue = 3.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
public double MaxSweepAtr { get; set; }
#endregion
#region 2. Tick-Volume Absorption Parameters
[Parameter("Volume Z-Score Lookback", DefaultValue = 20, MinValue = 5, MaxValue = 100, Group = "2. Tick Absorption Filter")]
public int VolumePeriod { get; set; }
[Parameter("Min Tick Volume Z-Score", DefaultValue = 1.25, MinValue = 0.0, MaxValue = 4.0, Step = 0.1, Group = "2. Tick Absorption Filter")]
public double MinVolumeZScore { get; set; }
[Parameter("Min Rejection Wick Ratio (0-1)", DefaultValue = 0.42, MinValue = 0.20, MaxValue = 0.85, Step = 0.02, Group = "2. Tick Absorption Filter")]
public double MinWickRatio { get; set; }
[Parameter("Verify M1 Intrabar Wick Volume", DefaultValue = true, Group = "2. Tick Absorption Filter")]
public bool UseM1IntrabarCheck { get; set; }
[Parameter("Min M1 Wick Volume Share (0-1)", DefaultValue = 0.35, MinValue = 0.10, MaxValue = 0.80, Step = 0.05, Group = "2. Tick Absorption Filter")]
public double MinM1WickVolumeShare { get; set; }
#endregion
#region 3. Seed Risk & House-Money Pyramiding Parameters
[Parameter("Seed Risk (% of Balance)", DefaultValue = 0.25, MinValue = 0.05, MaxValue = 2.0, Step = 0.05, Group = "3. House-Money Risk Engine")]
public double SeedRiskPercent { get; set; }
[Parameter("Stop Buffer Beyond Sweep (ATR)", DefaultValue = 0.20, MinValue = 0.05, MaxValue = 1.0, Step = 0.05, Group = "3. House-Money Risk Engine")]
public double StopBufferAtr { get; set; }
[Parameter("Breakeven Trigger (R-Multiple)", DefaultValue = 1.0, MinValue = 0.5, MaxValue = 3.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
public double BreakevenTriggerR { get; set; }
[Parameter("Breakeven Cushion (Pips)", DefaultValue = 1.5, MinValue = 0.2, MaxValue = 10.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
public double BreakevenCushionPips { get; set; }
[Parameter("Max Total Positions (Seed + Pyramids)", DefaultValue = 4, MinValue = 1, MaxValue = 10, Group = "3. House-Money Risk Engine")]
public int MaxBasketPositions { get; set; }
[Parameter("Pyramid Step Distance (R-Multiple)", DefaultValue = 1.6, MinValue = 1.0, MaxValue = 4.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
public double PyramidStepR { get; set; }
[Parameter("House-Money Risk Share (0.1 - 0.8)", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 0.85, Step = 0.05, Group = "3. House-Money Risk Engine")]
public double HouseMoneyRiskFraction { get; set; }
[Parameter("Hard Take-Profit on Seed (R-Multiple)", DefaultValue = 6.0, MinValue = 2.0, MaxValue = 20.0, Step = 0.5, Group = "3. House-Money Risk Engine")]
public double TargetTakeProfitR { get; set; }
#endregion
#region 4. Layer 7 ECN Execution Guards
[Parameter("Max Spread Multiplier (vs 60m Avg)", DefaultValue = 1.8, MinValue = 1.1, MaxValue = 4.0, Step = 0.1, Group = "4. ECN Kill Switches")]
public double MaxSpreadMultiplier { get; set; }
[Parameter("Max Market Range Slippage (Pips)", DefaultValue = 1.5, MinValue = 0.2, MaxValue = 10.0, Step = 0.1, Group = "4. ECN Kill Switches")]
public double MaxSlippagePips { get; set; }
#endregion
#region Internal State Structures
private sealed class LiquidityPool
{
public int BarIndex { get; init; }
public double Price { get; init; }
public bool IsSwingHigh { get; init; }
public bool IsMitigated { get; set; }
}
private readonly List<LiquidityPool> _activePools = new();
private readonly Queue<double> _rollingSpreads = new();
private AverageTrueRange _atr;
private Bars _m1Bars;
// Basket tracking state
private double _seedEntryPrice;
private double _seedInitialRiskPips;
private bool _isSeedBreakevenLocked;
#endregion
protected override void OnStart()
{
_atr = Indicators.AverageTrueRange(14, MovingAverageType.Exponential);
if (UseM1IntrabarCheck && Bars.TimeFrame > TimeFrame.Minute)
{
_m1Bars = MarketData.GetBars(TimeFrame.Minute);
}
// Warm up historical liquidity pools over the last 300 bars
int startWarmup = Math.Max(SwingStrength * 2 + VolumePeriod, Bars.Count - 300);
for (int i = startWarmup; i < Bars.Count - 1; i++)
{
RegisterConfirmedPools(i - SwingStrength);
PruneAndInvalidatePools(i);
}
Print("Chronos Sweep & House-Money Pyramid Engine started on {0} ({1}). Warmed up {2} active pools.",
SymbolName, TimeFrame, _activePools.Count);
}
protected override void OnTick()
{
RecordSpreadSample();
var basket = GetActiveBasket();
if (basket.Count == 0)
{
_isSeedBreakevenLocked = false;
return;
}
Position seedPos = basket[0];
if (_seedInitialRiskPips <= 0)
return;
// Phase 2: Check 1R Fortress Breakeven Lock on the Seed Position
if (!_isSeedBreakevenLocked)
{
double favorablePips = seedPos.TradeType == TradeType.Buy
? (Symbol.Bid - seedPos.EntryPrice) / Symbol.PipSize
: (seedPos.EntryPrice - Symbol.Ask) / Symbol.PipSize;
if (favorablePips >= _seedInitialRiskPips * BreakevenTriggerR)
{
LockSeedAtBreakeven(seedPos);
}
}
// Phase 4: Check Hard Basket Target R-Multiple
double totalRunPips = seedPos.TradeType == TradeType.Buy
? (Symbol.Bid - _seedEntryPrice) / Symbol.PipSize
: (_seedEntryPrice - Symbol.Ask) / Symbol.PipSize;
if (totalRunPips >= _seedInitialRiskPips * TargetTakeProfitR)
{
FlattenBasket($"Target {TargetTakeProfitR:F1}R Reached");
}
}
protected override void OnBar()
{
int closedIndex = Bars.Count - 2;
if (closedIndex <= SwingStrength * 2 + VolumePeriod)
return;
// 1. Register newly confirmed Swing High / Swing Low pivot
RegisterConfirmedPools(closedIndex - SwingStrength);
// 2. Evaluate closed bar for Liquidity Sweep + Tick Absorption
EvaluateClosedBar(closedIndex);
}
#region Core Strategy Logic (Sweep Detection & Pyramiding)
private void EvaluateClosedBar(int barIndex)
{
double open = Bars.OpenPrices[barIndex];
double high = Bars.HighPrices[barIndex];
double low = Bars.LowPrices[barIndex];
double close = Bars.ClosePrices[barIndex];
double range = high - low;
double atr = _atr.Result[barIndex];
if (range <= Symbol.TickSize || atr <= 0)
return;
LiquidityPool bullishSweepPool = null;
LiquidityPool bearishSweepPool = null;
for (int i = _activePools.Count - 1; i >= 0; i--)
{
LiquidityPool pool = _activePools[i];
if (barIndex - pool.BarIndex > MaxPoolAgeBars || pool.IsMitigated)
{
_activePools.RemoveAt(i);
continue;
}
if (pool.IsSwingHigh)
{
if (close > pool.Price)
{
pool.IsMitigated = true;
_activePools.RemoveAt(i);
continue;
}
double pen = high - pool.Price;
if (pen >= MinSweepAtr * atr && pen <= MaxSweepAtr * atr && close < pool.Price)
{
pool.IsMitigated = true;
bearishSweepPool = pool;
_activePools.RemoveAt(i);
}
}
else
{
if (close < pool.Price)
{
pool.IsMitigated = true;
_activePools.RemoveAt(i);
continue;
}
double pen = pool.Price - low;
if (pen >= MinSweepAtr * atr && pen <= MaxSweepAtr * atr && close > pool.Price)
{
pool.IsMitigated = true;
bullishSweepPool = pool;
_activePools.RemoveAt(i);
}
}
}
double volZScore = CalculateTickVolumeZScore(barIndex);
var basket = GetActiveBasket();
// Check Bullish Sweep Signal
bool validBullishSweep = false;
if (bullishSweepPool != null)
{
double lowerWickRatio = (Math.Min(open, close) - low) / range;
if (volZScore >= MinVolumeZScore &&
lowerWickRatio >= MinWickRatio &&
VerifyM1WickAbsorption(barIndex, bullishSweepPool.Price, isBullish: true))
{
validBullishSweep = true;
}
}
// Check Bearish Sweep Signal
bool validBearishSweep = false;
if (bearishSweepPool != null)
{
double upperWickRatio = (high - Math.Max(open, close)) / range;
if (volZScore >= MinVolumeZScore &&
upperWickRatio >= MinWickRatio &&
VerifyM1WickAbsorption(barIndex, bearishSweepPool.Price, isBullish: false))
{
validBearishSweep = true;
}
}
// If flat, execute Phase 1 Seed Probe
if (basket.Count == 0)
{
if (validBullishSweep && IsEcnSpreadHealthy())
{
double stopPrice = low - (atr * StopBufferAtr);
ExecuteSeedPosition(TradeType.Buy, stopPrice, bullishSweepPool);
}
else if (validBearishSweep && IsEcnSpreadHealthy())
{
double stopPrice = high + (atr * StopBufferAtr);
ExecuteSeedPosition(TradeType.Sell, stopPrice, bearishSweepPool);
}
return;
}
// If a basket is active, check for opposing sweep reversal or Phase 3 House-Money Pyramiding
TradeType basketDirection = basket[0].TradeType;
if ((basketDirection == TradeType.Buy && validBearishSweep) ||
(basketDirection == TradeType.Sell && validBullishSweep))
{
FlattenBasket("Opposing Institutional Liquidity Sweep Detected");
return;
}
if (_isSeedBreakevenLocked && basket.Count < MaxBasketPositions && IsEcnSpreadHealthy())
{
TryExecuteHouseMoneyPyramid(barIndex, basket, atr);
}
}
private void ExecuteSeedPosition(TradeType tradeType, double stopPrice, LiquidityPool pool)
{
double entryEstimate = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
double stopLossPips = Math.Abs(entryEstimate - stopPrice) / Symbol.PipSize;
if (stopLossPips < 2.0)
return;
double seedRiskCurrency = Account.Balance * (SeedRiskPercent / 100.0);
double rawUnits = seedRiskCurrency / (stopLossPips * Symbol.PipValue);
double volumeInUnits = Symbol.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);
if (volumeInUnits < Symbol.VolumeInUnitsMin)
return;
double takeProfitPips = Math.Round(stopLossPips * TargetTakeProfitR, 1);
TradeResult result = ExecuteMarketRangeOrder(
tradeType,
SymbolName,
volumeInUnits,
MaxSlippagePips,
entryEstimate,
BotLabel,
Math.Round(stopLossPips, 1),
takeProfitPips);
if (result.IsSuccessful && result.Position != null)
{
_seedEntryPrice = result.Position.EntryPrice;
_seedInitialRiskPips = stopLossPips;
_isSeedBreakevenLocked = false;
Chart.DrawTrendLine(
$"Sweep_{Bars.Count}",
Bars.OpenTimes[pool.BarIndex],
pool.Price,
Bars.OpenTimes[Bars.Count - 2],
pool.Price,
tradeType == TradeType.Buy ? Color.Lime : Color.Crimson,
2,
LineStyle.DotsRare);
Print("SEED PROBE OPENED | {0} {1} units @ {2:F5} | SL: {3:F1} pips | Risk: {4:C2} ({5:F2}%)",
tradeType, volumeInUnits, _seedEntryPrice, stopLossPips, seedRiskCurrency, SeedRiskPercent);
}
}
private void LockSeedAtBreakeven(Position seedPos)
{
double newStop = seedPos.TradeType == TradeType.Buy
? Symbol.NormalizePrice(seedPos.EntryPrice + (BreakevenCushionPips * Symbol.PipSize))
: Symbol.NormalizePrice(seedPos.EntryPrice - (BreakevenCushionPips * Symbol.PipSize));
bool isValidImprovement = seedPos.TradeType == TradeType.Buy
? (seedPos.StopLoss == null || newStop > seedPos.StopLoss.Value)
: (seedPos.StopLoss == null || newStop < seedPos.StopLoss.Value);
if (isValidImprovement)
{
var mod = ModifyPosition(seedPos, newStop, seedPos.TakeProfit);
if (mod.IsSuccessful)
{
_isSeedBreakevenLocked = true;
Print("1R FORTRESS LOCKED | Seed Position #{0} SL moved to {1:F5} (+{2:F1} pips cushion). Core equity risk = $0.00.",
seedPos.Id, newStop, BreakevenCushionPips);
}
}
}
private void TryExecuteHouseMoneyPyramid(int barIndex, List<Position> basket, double atr)
{
TradeType direction = basket[0].TradeType;
double currentPrice = direction == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
// 1. Verify price has advanced to the next R-step threshold for this pyramid layer
double requiredAdvancePips = _seedInitialRiskPips * (BreakevenTriggerR + (basket.Count * (PyramidStepR - 0.8)));
double currentAdvancePips = direction == TradeType.Buy
? (currentPrice - _seedEntryPrice) / Symbol.PipSize
: (_seedEntryPrice - currentPrice) / Symbol.PipSize;
if (currentAdvancePips < requiredAdvancePips)
return;
// 2. Confirm a continuation bar in the direction of the trend with rejection structure
bool isBullishContinuation = direction == TradeType.Buy &&
Bars.ClosePrices[barIndex] > Bars.OpenPrices[barIndex] &&
Bars.ClosePrices[barIndex] > Bars.HighPrices[barIndex - 1];
bool isBearishContinuation = direction == TradeType.Sell &&
Bars.ClosePrices[barIndex] < Bars.OpenPrices[barIndex] &&
Bars.ClosePrices[barIndex] < Bars.LowPrices[barIndex - 1];
if (!isBullishContinuation && !isBearishContinuation)
return;
// 3. Calculate the proposed new Unified Structural Stop Loss behind the continuation pullback
double proposedStop = direction == TradeType.Buy
? Symbol.NormalizePrice(Math.Min(Bars.LowPrices[barIndex], Bars.LowPrices[barIndex - 1]) - (atr * StopBufferAtr))
: Symbol.NormalizePrice(Math.Max(Bars.HighPrices[barIndex], Bars.HighPrices[barIndex - 1]) + (atr * StopBufferAtr));
// Ensure the proposed stop strictly ratchets forward for every open leg in the basket
foreach (Position pos in basket)
{
if (pos.StopLoss == null) return;
if (direction == TradeType.Buy && proposedStop <= pos.StopLoss.Value) return;
if (direction == TradeType.Sell && proposedStop >= pos.StopLoss.Value) return;
}
// 4. Calculate Total Guaranteed Locked Profit (in Account Currency) if all existing legs are trailed to proposedStop
double totalLockedProfitCurrency = 0.0;
foreach (Position pos in basket)
{
double lockedPips = direction == TradeType.Buy
? (proposedStop - pos.EntryPrice) / Symbol.PipSize
: (pos.EntryPrice - proposedStop) / Symbol.PipSize;
double legLockedGross = lockedPips * Symbol.PipValue * pos.VolumeInUnits;
double legNetLocked = legLockedGross + (pos.Commissions * 2.0) + pos.Swap;
totalLockedProfitCurrency += legNetLocked;
}
if (totalLockedProfitCurrency <= 0)
return;
// 5. Allocate only HouseMoneyRiskFraction (e.g., 50%) of the guaranteed locked profit to the new leg
double houseMoneyRiskBudget = totalLockedProfitCurrency * HouseMoneyRiskFraction;
double newLegStopPips = Math.Abs(currentPrice - proposedStop) / Symbol.PipSize;
if (newLegStopPips < 2.0)
return;
double rawPyramidUnits = houseMoneyRiskBudget / (newLegStopPips * Symbol.PipValue);
double pyramidVolumeUnits = Symbol.NormalizeVolumeInUnits(rawPyramidUnits, RoundingMode.Down);
if (pyramidVolumeUnits < Symbol.VolumeInUnitsMin)
return;
// 6. First ratchet all existing positions to the new Unified Stop Loss
foreach (Position pos in basket)
{
var mod = ModifyPosition(pos, proposedStop, pos.TakeProfit);
if (!mod.IsSuccessful)
return; // Abort pyramid entry if any existing leg fails to lock its stop
}
// 7. Fire the House-Money Pyramid Order with the exact same unified stop
TradeResult pyrResult = ExecuteMarketRangeOrder(
direction,
SymbolName,
pyramidVolumeUnits,
MaxSlippagePips,
currentPrice,
BotLabel,
Math.Round(newLegStopPips, 1),
null);
if (pyrResult.IsSuccessful && pyrResult.Position != null)
{
// Align exact stop price with the rest of the basket
ModifyPosition(pyrResult.Position, proposedStop, basket[0].TakeProfit);
Print("HOUSE-MONEY PYRAMID LEG #{0} FIRED | {1} {2} units @ {3:F5} | Unified SL: {4:F5} | Locked Profit Buffer: {5:C2} | Allocated Risk: {6:C2} (Worst-case Net: +{7:C2})",
basket.Count + 1,
direction,
pyramidVolumeUnits,
pyrResult.Position.EntryPrice,
proposedStop,
totalLockedProfitCurrency,
houseMoneyRiskBudget,
totalLockedProfitCurrency - houseMoneyRiskBudget);
}
}
#endregion
#region Helper & Microstructure Verification Methods
private void RegisterConfirmedPools(int pivotIndex)
{
if (pivotIndex < SwingStrength)
return;
double candidateHigh = Bars.HighPrices[pivotIndex];
double candidateLow = Bars.LowPrices[pivotIndex];
bool isSwingHigh = true;
bool isSwingLow = true;
for (int offset = 1; offset <= SwingStrength; offset++)
{
if (Bars.HighPrices[pivotIndex - offset] >= candidateHigh ||
Bars.HighPrices[pivotIndex + offset] > candidateHigh)
isSwingHigh = false;
if (Bars.LowPrices[pivotIndex - offset] <= candidateLow ||
Bars.LowPrices[pivotIndex + offset] < candidateLow)
isSwingLow = false;
}
if (isSwingHigh)
_activePools.Add(new LiquidityPool { BarIndex = pivotIndex, Price = candidateHigh, IsSwingHigh = true });
if (isSwingLow)
_activePools.Add(new LiquidityPool { BarIndex = pivotIndex, Price = candidateLow, IsSwingHigh = false });
}
private void PruneAndInvalidatePools(int barIndex)
{
double close = Bars.ClosePrices[barIndex];
for (int i = _activePools.Count - 1; i >= 0; i--)
{
var pool = _activePools[i];
if (barIndex - pool.BarIndex > MaxPoolAgeBars ||
(pool.IsSwingHigh && close > pool.Price) ||
(!pool.IsSwingHigh && close < pool.Price))
{
_activePools.RemoveAt(i);
}
}
}
private double CalculateTickVolumeZScore(int barIndex)
{
double sum = 0.0;
for (int i = 0; i < VolumePeriod; i++)
sum += Bars.TickVolumes[barIndex - i];
double mean = sum / VolumePeriod;
double varSum = 0.0;
for (int i = 0; i < VolumePeriod; i++)
{
double diff = Bars.TickVolumes[barIndex - i] - mean;
varSum += diff * diff;
}
double stdDev = Math.Sqrt(varSum / VolumePeriod);
return stdDev <= 1e-5 ? 0.0 : (Bars.TickVolumes[barIndex] - mean) / stdDev;
}
private bool VerifyM1WickAbsorption(int m15Index, double sweptPoolPrice, bool isBullish)
{
if (!UseM1IntrabarCheck || _m1Bars == null || _m1Bars.Count == 0)
return true;
DateTime barStart = Bars.OpenTimes[m15Index];
DateTime barEnd = m15Index + 1 < Bars.Count
? Bars.OpenTimes[m15Index + 1]
: barStart.AddMinutes(15);
int startM1Idx = _m1Bars.OpenTimes.GetIndexByTime(barStart);
if (startM1Idx < 0 || _m1Bars.OpenTimes[startM1Idx] < barStart.AddMinutes(-15))
return true;
double totalM1Volume = 0;
double wickZoneVolume = 0;
for (int m1 = startM1Idx; m1 < _m1Bars.Count; m1++)
{
DateTime t = _m1Bars.OpenTimes[m1];
if (t < barStart) continue;
if (t >= barEnd) break;
double v = _m1Bars.TickVolumes[m1];
totalM1Volume += v;
if (isBullish && _m1Bars.LowPrices[m1] <= sweptPoolPrice)
wickZoneVolume += v;
else if (!isBullish && _m1Bars.HighPrices[m1] >= sweptPoolPrice)
wickZoneVolume += v;
}
return totalM1Volume <= 0 || (wickZoneVolume / totalM1Volume) >= MinM1WickVolumeShare;
}
private void RecordSpreadSample()
{
_rollingSpreads.Enqueue(Symbol.Spread);
if (_rollingSpreads.Count > 500)
_rollingSpreads.Dequeue();
}
private bool IsEcnSpreadHealthy()
{
if (_rollingSpreads.Count < 30)
return true;
double avgSpread = _rollingSpreads.Average();
return Symbol.Spread <= avgSpread * MaxSpreadMultiplier;
}
private List<Position> GetActiveBasket()
{
return Positions
.Where(p => p.SymbolName == SymbolName && p.Label == BotLabel)
.OrderBy(p => p.EntryTime)
.ToList();
}
private void FlattenBasket(string reason)
{
var basket = GetActiveBasket();
foreach (Position pos in basket)
ClosePosition(pos);
_isSeedBreakevenLocked = false;
Print("BASKET FLATTENED ({0} positions) | Reason: {1}", basket.Count, reason);
}
#endregion
}
}