Checklist fatigue: how I kept my pre-trade list to five lines
Re: Checklist fatigue: how I kept my pre-trade list to five lines
The bot now independently protects your capital as soon as momentum accelerates in your favor. When the trade moves 1R into profit (or whatever decimal you assign to BreakevenTriggerRR), it will fire off the ModifyPositionAsync command to IC Markets, locking in the commission buffer.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
To mathematically implement your exponential series strategy—specifically locking the original principal and scaling strictly with "house money"—we need to shift away from static lot sizes and build a dynamic volume calculator based on account equity relative to your starting balance.
The logic relies on measuring Accrued Profit (Account.Equity - SeriesStartingBalance).
I have added a strict Lock Principal toggle. When enabled, the moment your account enters profit, the bot drops your base risk to zero and calculates the lot size exclusively using a percentage of your accrued profits. If you take a loss and drop back to the starting balance, it reverts to the small base risk.
The logic relies on measuring Accrued Profit (Account.Equity - SeriesStartingBalance).
I have added a strict Lock Principal toggle. When enabled, the moment your account enters profit, the bot drops your base risk to zero and calculates the lot size exclusively using a percentage of your accrued profits. If you take a loss and drop back to the starting balance, it reverts to the small base risk.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the fully updated cBot with the exponential risk engine integrated into the execution module:
Code: Select all
using System;
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 LiquiditySweepsEQHEQL_Exponential : Robot
{
[Header("Structural Mapping")]
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("EQH/EQL Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Header("Exponential Risk Management")]
[Parameter("Use Exponential Scaling", DefaultValue = true)]
public bool UseExponentialScaling { get; set; }
[Parameter("Series Starting Balance", DefaultValue = 1000.0)]
public double SeriesStartingBalance { get; set; }
[Parameter("Base Risk (Currency)", DefaultValue = 20.0)]
public double BaseRiskAmount { get; set; }
[Parameter("Profit Reinvestment (%)", DefaultValue = 50.0, MinValue = 0, MaxValue = 100)]
public double ProfitScalingPercent { get; set; }
[Parameter("Lock Principal (Risk ONLY Profits)", DefaultValue = true)]
public bool LockPrincipal { get; set; }
[Parameter("Static Fallback Volume (Lots)", DefaultValue = 1.0)]
public double StaticQuantityInLots { get; set; }
[Header("Execution Rules")]
[Parameter("Stop Loss (Pips)", DefaultValue = 10.0)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (Pips)", DefaultValue = 30.0)]
public double TakeProfitPips { get; set; }
[Parameter("Max Spread (Pips)", DefaultValue = 1.5)]
public double MaxSpreadPips { get; set; }
[Header("Trade Management (Breakeven)")]
[Parameter("Use Auto Breakeven", DefaultValue = true)]
public bool UseBreakeven { get; set; }
[Parameter("Trigger at R:R (1.0 = 1:1 Target)", DefaultValue = 1.0)]
public double BreakevenTriggerRR { get; set; }
[Parameter("Commission Buffer (Pips)", DefaultValue = 0.6)]
public double CommissionBufferPips { get; set; }
private Color _h4Color = Color.DarkOrange;
private Color _d1Color = Color.Crimson;
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
protected override void OnTick()
{
if (!UseBreakeven) return;
var positions = Positions.FindAll("LiqSweep", SymbolName);
foreach (var position in positions)
{
double triggerPips = StopLossPips * BreakevenTriggerRR;
double profitInPips = position.TradeType == TradeType.Buy
? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize
: (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;
if (profitInPips >= triggerPips)
{
double newStopLoss = position.TradeType == TradeType.Buy
? position.EntryPrice + (CommissionBufferPips * Symbol.PipSize)
: position.EntryPrice - (CommissionBufferPips * Symbol.PipSize);
bool requiresUpdate = false;
if (position.TradeType == TradeType.Buy && (position.StopLoss == null || position.StopLoss < newStopLoss - (0.1 * Symbol.PipSize)))
requiresUpdate = true;
if (position.TradeType == TradeType.Sell && (position.StopLoss == null || position.StopLoss > newStopLoss + (0.1 * Symbol.PipSize)))
requiresUpdate = true;
if (requiresUpdate)
{
ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
}
}
}
}
protected override void OnBar()
{
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines) Chart.RemoveObject(line.Name);
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
List<double> d1EQH, d1EQL, h4EQH, h4EQL;
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2, out d1EQH, out d1EQL);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1, out h4EQH, out h4EQL);
var activeEQH = d1EQH.Concat(h4EQH).ToList();
var activeEQL = d1EQL.Concat(h4EQL).ToList();
EvaluateSweepExecution(activeEQH, activeEQL);
}
private void EvaluateSweepExecution(List<double> activeEQH, List<double> activeEQL)
{
if (Positions.FindAll("LiqSweep", SymbolName).Length > 0) return;
if ((Symbol.Spread / Symbol.PipSize) > MaxSpreadPips) return;
var lastBar = Bars.Last(1);
// Generate dynamic volume based on accrued floating profit
double volumeInUnits = GetDynamicVolume();
// Circuit breaker: abort if calculated risk falls below IC Markets minimum lot size
if (volumeInUnits < Symbol.VolumeInUnitsMin) return;
foreach (var eqh in activeEQH)
{
if (lastBar.Open < eqh && lastBar.High > eqh && lastBar.Close < eqh)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
foreach (var eql in activeEQL)
{
if (lastBar.Open > eql && lastBar.Low < eql && lastBar.Close > eql)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
}
// --- Core Exponential Risk Engine ---
private double GetDynamicVolume()
{
if (!UseExponentialScaling)
return Symbol.QuantityToVolumeInUnits(StaticQuantityInLots);
double accruedProfit = Account.Equity - SeriesStartingBalance;
double riskAmountCurrency = BaseRiskAmount;
if (accruedProfit > 0)
{
if (LockPrincipal)
{
// Principal is safe. Risk strictly scales from the accrued floating gains.
riskAmountCurrency = accruedProfit * (ProfitScalingPercent / 100.0);
}
else
{
// Compounding: Base risk + a percentage of accrued gains
riskAmountCurrency = BaseRiskAmount + (accruedProfit * (ProfitScalingPercent / 100.0));
}
}
// Convert currency risk into actual volume based on Stop Loss distance
double pipValuePerUnit = Symbol.PipValue;
double calculatedUnits = riskAmountCurrency / (StopLossPips * pipValuePerUnit);
// Floor the volume to ensure it maps perfectly to IC Markets lot steps
return Symbol.NormalizeVolumeInUnits(calculatedUnits, RoundingMode.Down);
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width, out List<double> activeEQH, out List<double> activeEQL)
{
activeEQH = new List<double>();
activeEQL = new List<double>();
var highs = new List<SwingPoint>();
var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true, isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j]) isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j]) isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i], Used = false });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i], Used = false });
}
double tolerance = TolerancePips * Symbol.PipSize;
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
DrawLine($"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}", highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
activeEQH.Add(highs[i].Price);
highs[i].Used = true; highs[j].Used = true;
break;
}
}
}
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
DrawLine($"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}", lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
activeEQL.Add(lows[i].Price);
lows[i].Used = true; lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false; line.ExtendToRight = true;
}
}
}Re: Checklist fatigue: how I kept my pre-trade list to five lines
To implement a trailing stop that respects your breakeven rule, the logic must evaluate three possible Stop Loss levels on every price tick and dynamically select the highest (for longs) or lowest (for shorts) among them:
1.) Current Stop Loss (Where it is right now)
2.) Breakeven Stop Loss (Entry ± Commission Buffer)
3.) Trailing Stop Loss (Current Price ± Trailing Distance)
By running this through a Math.Max() or Math.Min() comparison, the bot guarantees it will never move the stop loss backwards, effectively locking the breakeven floor as soon as 1R is hit, and trailing from there.
1.) Current Stop Loss (Where it is right now)
2.) Breakeven Stop Loss (Entry ± Commission Buffer)
3.) Trailing Stop Loss (Current Price ± Trailing Distance)
By running this through a Math.Max() or Math.Min() comparison, the bot guarantees it will never move the stop loss backwards, effectively locking the breakeven floor as soon as 1R is hit, and trailing from there.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Crucially, you must introduce a Trailing Step. If you trail tick-by-tick, you will flood IC Markets' servers with thousands of modification requests per minute, which can result in temporary API blocks. A step size (e.g., 2 pips) ensures the bot only moves the stop once price has advanced meaningfully.
Here is the fully integrated cBot with the dynamic Trailing Stop module:
Here is the fully integrated cBot with the dynamic Trailing Stop module:
Code: Select all
using System;
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 LiquiditySweepsEQHEQL_ProExec : Robot
{
[Header("Structural Mapping")]
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("EQH/EQL Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Header("Exponential Risk Management")]
[Parameter("Use Exponential Scaling", DefaultValue = true)]
public bool UseExponentialScaling { get; set; }
[Parameter("Series Starting Balance", DefaultValue = 1000.0)]
public double SeriesStartingBalance { get; set; }
[Parameter("Base Risk (Currency)", DefaultValue = 20.0)]
public double BaseRiskAmount { get; set; }
[Parameter("Profit Reinvestment (%)", DefaultValue = 50.0, MinValue = 0, MaxValue = 100)]
public double ProfitScalingPercent { get; set; }
[Parameter("Lock Principal (Risk ONLY Profits)", DefaultValue = true)]
public bool LockPrincipal { get; set; }
[Parameter("Static Fallback Volume (Lots)", DefaultValue = 1.0)]
public double StaticQuantityInLots { get; set; }
[Header("Execution Rules")]
[Parameter("Stop Loss (Pips)", DefaultValue = 10.0)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (Pips)", DefaultValue = 50.0)]
public double TakeProfitPips { get; set; }
[Parameter("Max Spread (Pips)", DefaultValue = 1.5)]
public double MaxSpreadPips { get; set; }
[Header("Trade Management (BE & Trail)")]
[Parameter("Use Auto Breakeven & Trail", DefaultValue = true)]
public bool UseTradeManagement { get; set; }
[Parameter("Trigger BE at R:R (1.0 = 1:1 Target)", DefaultValue = 1.0)]
public double BreakevenTriggerRR { get; set; }
[Parameter("Commission Buffer (Pips)", DefaultValue = 0.6)]
public double CommissionBufferPips { get; set; }
[Parameter("Use Trailing Stop After BE", DefaultValue = true)]
public bool UseTrailingStop { get; set; }
[Parameter("Trailing Distance (Pips)", DefaultValue = 15.0)]
public double TrailingDistancePips { get; set; }
[Parameter("Trailing Step (Pips)", DefaultValue = 2.0)]
public double TrailingStepPips { get; set; }
private Color _h4Color = Color.DarkOrange;
private Color _d1Color = Color.Crimson;
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
protected override void OnTick()
{
if (!UseTradeManagement) return;
var positions = Positions.FindAll("LiqSweep", SymbolName);
foreach (var position in positions)
{
double beTriggerPips = StopLossPips * BreakevenTriggerRR;
double profitInPips = position.TradeType == TradeType.Buy
? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize
: (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;
// Management only activates once Breakeven threshold is crossed
if (profitInPips >= beTriggerPips)
{
double currentSL = position.StopLoss ?? (position.TradeType == TradeType.Buy ? double.MinValue : double.MaxValue);
// 1. Calculate Breakeven floor
double beStopLoss = position.TradeType == TradeType.Buy
? position.EntryPrice + (CommissionBufferPips * Symbol.PipSize)
: position.EntryPrice - (CommissionBufferPips * Symbol.PipSize);
double targetSL = position.TradeType == TradeType.Buy
? Math.Max(currentSL, beStopLoss)
: Math.Min(currentSL, beStopLoss);
// 2. Calculate Trailing Stop (if enabled)
if (UseTrailingStop)
{
double trailingSL = position.TradeType == TradeType.Buy
? Symbol.Bid - (TrailingDistancePips * Symbol.PipSize)
: Symbol.Ask + (TrailingDistancePips * Symbol.PipSize);
targetSL = position.TradeType == TradeType.Buy
? Math.Max(targetSL, trailingSL)
: Math.Min(targetSL, trailingSL);
}
// 3. Execution with Step Buffer to prevent server spam
double stepBuffer = TrailingStepPips * Symbol.PipSize;
bool requiresUpdate = false;
if (position.TradeType == TradeType.Buy && targetSL > currentSL + stepBuffer)
{
requiresUpdate = true;
}
else if (position.TradeType == TradeType.Sell && targetSL < currentSL - stepBuffer)
{
requiresUpdate = true;
}
if (requiresUpdate)
{
ModifyPositionAsync(position, Math.Round(targetSL, Symbol.Digits), position.TakeProfit);
}
}
}
}
protected override void OnBar()
{
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines) Chart.RemoveObject(line.Name);
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
List<double> d1EQH, d1EQL, h4EQH, h4EQL;
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2, out d1EQH, out d1EQL);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1, out h4EQH, out h4EQL);
var activeEQH = d1EQH.Concat(h4EQH).ToList();
var activeEQL = d1EQL.Concat(h4EQL).ToList();
EvaluateSweepExecution(activeEQH, activeEQL);
}
private void EvaluateSweepExecution(List<double> activeEQH, List<double> activeEQL)
{
if (Positions.FindAll("LiqSweep", SymbolName).Length > 0) return;
if ((Symbol.Spread / Symbol.PipSize) > MaxSpreadPips) return;
var lastBar = Bars.Last(1);
double volumeInUnits = GetDynamicVolume();
if (volumeInUnits < Symbol.VolumeInUnitsMin) return;
foreach (var eqh in activeEQH)
{
if (lastBar.Open < eqh && lastBar.High > eqh && lastBar.Close < eqh)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
foreach (var eql in activeEQL)
{
if (lastBar.Open > eql && lastBar.Low < eql && lastBar.Close > eql)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
}
private double GetDynamicVolume()
{
if (!UseExponentialScaling) return Symbol.QuantityToVolumeInUnits(StaticQuantityInLots);
double accruedProfit = Account.Equity - SeriesStartingBalance;
double riskAmountCurrency = BaseRiskAmount;
if (accruedProfit > 0)
{
riskAmountCurrency = LockPrincipal
? accruedProfit * (ProfitScalingPercent / 100.0)
: BaseRiskAmount + (accruedProfit * (ProfitScalingPercent / 100.0));
}
double pipValuePerUnit = Symbol.PipValue;
double calculatedUnits = riskAmountCurrency / (StopLossPips * pipValuePerUnit);
return Symbol.NormalizeVolumeInUnits(calculatedUnits, RoundingMode.Down);
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width, out List<double> activeEQH, out List<double> activeEQL)
{
activeEQH = new List<double>(); activeEQL = new List<double>();
var highs = new List<SwingPoint>(); var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true, isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j]) isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j]) isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i], Used = false });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i], Used = false });
}
double tolerance = TolerancePips * Symbol.PipSize;
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
DrawLine($"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}", highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
activeEQH.Add(highs[i].Price);
highs[i].Used = true; highs[j].Used = true;
break;
}
}
}
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
DrawLine($"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}", lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
activeEQL.Add(lows[i].Price);
lows[i].Used = true; lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false; line.ExtendToRight = true;
}
}
}Re: Checklist fatigue: how I kept my pre-trade list to five lines
To execute a partial close in cTrader, you do not open multiple smaller trades. Instead, you use ClosePositionAsync(position, volumeToClose) to dynamically shave volume off the active position ID.
The critical architectural challenge here is the Tick Loop Trap. Because OnTick() evaluates conditions multiple times per second, the moment your trade crosses the 1R threshold, the bot will attempt to close 50% of the trade on every single price tick until the position vanishes. To prevent this, you must introduce a state-tracking HashSet to memorize which position IDs have already been partially closed.
The critical architectural challenge here is the Tick Loop Trap. Because OnTick() evaluates conditions multiple times per second, the moment your trade crosses the 1R threshold, the bot will attempt to close 50% of the trade on every single price tick until the position vanishes. To prevent this, you must introduce a state-tracking HashSet to memorize which position IDs have already been partially closed.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the fully integrated master cBot, combining the exponential risk engine, trailing stop, and the new dynamic partial profit module:
Code: Select all
using System;
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 LiquiditySweepsEQHEQL_Master : Robot
{
[Header("Structural Mapping")]
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("EQH/EQL Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Header("Exponential Risk Management")]
[Parameter("Use Exponential Scaling", DefaultValue = true)]
public bool UseExponentialScaling { get; set; }
[Parameter("Series Starting Balance", DefaultValue = 1000.0)]
public double SeriesStartingBalance { get; set; }
[Parameter("Base Risk (Currency)", DefaultValue = 20.0)]
public double BaseRiskAmount { get; set; }
[Parameter("Profit Reinvestment (%)", DefaultValue = 50.0, MinValue = 0, MaxValue = 100)]
public double ProfitScalingPercent { get; set; }
[Parameter("Lock Principal", DefaultValue = true)]
public bool LockPrincipal { get; set; }
[Parameter("Static Fallback Volume (Lots)", DefaultValue = 1.0)]
public double StaticQuantityInLots { get; set; }
[Header("Execution Rules")]
[Parameter("Stop Loss (Pips)", DefaultValue = 10.0)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (Pips)", DefaultValue = 50.0)]
public double TakeProfitPips { get; set; }
[Parameter("Max Spread (Pips)", DefaultValue = 1.5)]
public double MaxSpreadPips { get; set; }
[Header("Trade Management (Partial, BE & Trail)")]
[Parameter("Trigger Management at R:R", DefaultValue = 1.0)]
public double ManagementTriggerRR { get; set; }
[Parameter("Take Partial Profit", DefaultValue = true)]
public bool UsePartialProfit { get; set; }
[Parameter("Partial Profit (%)", DefaultValue = 50.0)]
public double PartialProfitPercent { get; set; }
[Parameter("Move to Breakeven", DefaultValue = true)]
public bool UseBreakeven { get; set; }
[Parameter("Commission Buffer (Pips)", DefaultValue = 0.6)]
public double CommissionBufferPips { get; set; }
[Parameter("Use Trailing Stop After BE", DefaultValue = true)]
public bool UseTrailingStop { get; set; }
[Parameter("Trailing Distance (Pips)", DefaultValue = 15.0)]
public double TrailingDistancePips { get; set; }
[Parameter("Trailing Step (Pips)", DefaultValue = 2.0)]
public double TrailingStepPips { get; set; }
private Color _h4Color = Color.DarkOrange;
private Color _d1Color = Color.Crimson;
// State tracker to prevent infinite partial-close loops
private HashSet<int> _partiallyClosedIds = new HashSet<int>();
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
protected override void OnTick()
{
var positions = Positions.FindAll("LiqSweep", SymbolName);
foreach (var position in positions)
{
double triggerPips = StopLossPips * ManagementTriggerRR;
double profitInPips = position.TradeType == TradeType.Buy
? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize
: (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;
if (profitInPips >= triggerPips)
{
// 1. Execute Partial Close (One-Time Execution per Position ID)
if (UsePartialProfit && !_partiallyClosedIds.Contains(position.Id))
{
double volumeToClose = position.VolumeInUnits * (PartialProfitPercent / 100.0);
volumeToClose = Symbol.NormalizeVolumeInUnits(volumeToClose, RoundingMode.Down);
if (volumeToClose >= Symbol.VolumeInUnitsMin)
{
ClosePositionAsync(position, volumeToClose);
_partiallyClosedIds.Add(position.Id);
Print($"Partial {PartialProfitPercent}% closed for position {position.Id} at {ManagementTriggerRR} R:R.");
}
}
// 2. Execute Breakeven & Trailing Logic
if (UseBreakeven)
{
double currentSL = position.StopLoss ?? (position.TradeType == TradeType.Buy ? double.MinValue : double.MaxValue);
double beStopLoss = position.TradeType == TradeType.Buy
? position.EntryPrice + (CommissionBufferPips * Symbol.PipSize)
: position.EntryPrice - (CommissionBufferPips * Symbol.PipSize);
double targetSL = position.TradeType == TradeType.Buy ? Math.Max(currentSL, beStopLoss) : Math.Min(currentSL, beStopLoss);
if (UseTrailingStop)
{
double trailingSL = position.TradeType == TradeType.Buy
? Symbol.Bid - (TrailingDistancePips * Symbol.PipSize)
: Symbol.Ask + (TrailingDistancePips * Symbol.PipSize);
targetSL = position.TradeType == TradeType.Buy ? Math.Max(targetSL, trailingSL) : Math.Min(targetSL, trailingSL);
}
double stepBuffer = TrailingStepPips * Symbol.PipSize;
bool requiresUpdate = false;
if (position.TradeType == TradeType.Buy && targetSL > currentSL + stepBuffer) requiresUpdate = true;
else if (position.TradeType == TradeType.Sell && targetSL < currentSL - stepBuffer) requiresUpdate = true;
if (requiresUpdate) ModifyPositionAsync(position, Math.Round(targetSL, Symbol.Digits), position.TakeProfit);
}
}
}
}
protected override void OnBar()
{
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines) Chart.RemoveObject(line.Name);
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
List<double> d1EQH, d1EQL, h4EQH, h4EQL;
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2, out d1EQH, out d1EQL);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1, out h4EQH, out h4EQL);
var activeEQH = d1EQH.Concat(h4EQH).ToList();
var activeEQL = d1EQL.Concat(h4EQL).ToList();
EvaluateSweepExecution(activeEQH, activeEQL);
}
private void EvaluateSweepExecution(List<double> activeEQH, List<double> activeEQL)
{
if (Positions.FindAll("LiqSweep", SymbolName).Length > 0) return;
if ((Symbol.Spread / Symbol.PipSize) > MaxSpreadPips) return;
var lastBar = Bars.Last(1);
double volumeInUnits = GetDynamicVolume();
if (volumeInUnits < Symbol.VolumeInUnitsMin) return;
foreach (var eqh in activeEQH)
{
if (lastBar.Open < eqh && lastBar.High > eqh && lastBar.Close < eqh)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
foreach (var eql in activeEQL)
{
if (lastBar.Open > eql && lastBar.Low < eql && lastBar.Close > eql)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
}
private double GetDynamicVolume()
{
if (!UseExponentialScaling) return Symbol.QuantityToVolumeInUnits(StaticQuantityInLots);
double accruedProfit = Account.Equity - SeriesStartingBalance;
double riskAmountCurrency = BaseRiskAmount;
if (accruedProfit > 0)
{
riskAmountCurrency = LockPrincipal
? accruedProfit * (ProfitScalingPercent / 100.0)
: BaseRiskAmount + (accruedProfit * (ProfitScalingPercent / 100.0));
}
double calculatedUnits = riskAmountCurrency / (StopLossPips * Symbol.PipValue);
return Symbol.NormalizeVolumeInUnits(calculatedUnits, RoundingMode.Down);
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width, out List<double> activeEQH, out List<double> activeEQL)
{
activeEQH = new List<double>(); activeEQL = new List<double>();
var highs = new List<SwingPoint>(); var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true, isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j]) isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j]) isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i], Used = false });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i], Used = false });
}
double tolerance = TolerancePips * Symbol.PipSize;
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
DrawLine($"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}", highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
activeEQH.Add(highs[i].Price);
highs[i].Used = true; highs[j].Used = true;
break;
}
}
}
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
DrawLine($"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}", lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
activeEQL.Add(lows[i].Price);
lows[i].Used = true; lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false; line.ExtendToRight = true;
}
}
}Re: Checklist fatigue: how I kept my pre-trade list to five lines
Volume Normalization: The partial close calculation runs through Symbol.NormalizeVolumeInUnits(..., RoundingMode.Down). If you scale into an odd lot size (e.g., 1.35 lots), halving it produces 0.675 lots. IC Markets rejects that execution. Normalization floors it cleanly to the nearest micro-lot (0.67 lots).
Independent Toggles: You can now choose to execute the 50% partial close, the breakeven stop, the trailing stop, or all three simultaneously using the exact same R:R trigger threshold.
Independent Toggles: You can now choose to execute the 50% partial close, the breakeven stop, the trailing stop, or all three simultaneously using the exact same R:R trigger threshold.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Translating this logic into Pine Script v5 requires a complete architectural shift. Because TradingView runs in a cloud-based, time-series environment, we cannot loop through 500 bars of history on every single tick like we did in C# or MQL5—that will trigger a script timeout.
Instead, this Pine Script is event-driven. It monitors the H4 and D1 timeframes natively, detects when a new structural swing is confirmed, and stores it in memory. It then checks that new swing against historical ones. When an EQH/EQL is paired, it projects the line and actively monitors the M15 candles for the sweep trap.
Instead, this Pine Script is event-driven. It monitors the H4 and D1 timeframes natively, detects when a new structural swing is confirmed, and stores it in memory. It then checks that new swing against historical ones. When an EQH/EQL is paired, it projects the line and actively monitors the M15 candles for the sweep trap.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Because TradingView cannot execute trades natively on IC Markets, this script uses Visual Shapes and dynamic Alerts. You can link these alerts to a webhook service (like PineConnector or a custom cBot endpoint) to retain the auto-execution.
Add this directly to your Pine Editor:
Add this directly to your Pine Editor:
Code: Select all
//@version=5
indicator("HTF Liquidity Sweeps (EQH/EQL) [Auto-Exec]", overlay=true, max_lines_count=500)
// === INPUTS ===
grp1 = "Structural Mapping"
swingBars = input.int(5, "Swing Bars (Left & Right)", group=grp1)
tolPips = input.float(15.0, "EQH/EQL Tolerance (Pips)", group=grp1)
h4Color = input.color(color.new(color.orange, 0), "H4 Color", group=grp1)
d1Color = input.color(color.new(color.red, 0), "D1 Color", group=grp1)
grp2 = "Execution Triggers"
alertOnSweep = input.bool(true, "Fire Alert on M15 Sweep", group=grp2)
// Pip Calculation (Dynamically scales for IC Markets 5-digit forex)
pip = syminfo.mintick * (syminfo.type == "forex" and (syminfo.mintick == 0.00001 or syminfo.mintick == 0.001) ? 10 : 1)
tol = tolPips * pip
// === CUSTOM TYPES & STATE ===
type SwingPoint
int t
float p
bool used
var h4_highs = array.new<SwingPoint>()
var h4_lows = array.new<SwingPoint>()
var d1_highs = array.new<SwingPoint>()
var d1_lows = array.new<SwingPoint>()
// Tracks active liquidity levels waiting to be swept
var activeEQH = array.new_float()
var activeEQL = array.new_float()
// === CORE LOGIC FUNCTION ===
processPivots(float ph, int ph_time, float pl, int pl_time, array<SwingPoint> arr_highs, array<SwingPoint> arr_lows, color line_col, string tf_prefix) =>
// Process Highs (EQH)
if not na(ph)
array.push(arr_highs, SwingPoint.new(ph_time, ph, false))
size = array.size(arr_highs)
if size > 1
for i = size - 2 to 0
past = array.get(arr_highs, i)
if not past.used and math.abs(past.p - ph) <= tol
past.used := true
array.get(arr_highs, size - 1).used := true
line.new(x1=past.t, y1=past.p, x2=ph_time, y2=ph, xloc=xloc.bar_time, color=line_col, style=line.style_solid, width=2, extend=extend.right)
array.push(activeEQH, ph)
break
// Process Lows (EQL)
if not na(pl)
array.push(arr_lows, SwingPoint.new(pl_time, pl, false))
size = array.size(arr_lows)
if size > 1
for i = size - 2 to 0
past = array.get(arr_lows, i)
if not past.used and math.abs(past.p - pl) <= tol
past.used := true
array.get(arr_lows, size - 1).used := true
line.new(x1=past.t, y1=past.p, x2=pl_time, y2=pl, xloc=xloc.bar_time, color=line_col, style=line.style_solid, width=2, extend=extend.right)
array.push(activeEQL, pl)
break
// === HTF DATA RETRIEVAL ===
// Lookahead is OFF to prevent repainting. Pivots only confirm after the right shoulder closes.
[h4_ph, h4_ph_time, h4_pl, h4_pl_time] = request.security(syminfo.tickerid, "240", [ta.pivothigh(high, swingBars, swingBars), time[swingBars], ta.pivotlow(low, swingBars, swingBars), time[swingBars]], lookahead=barmerge.lookahead_off)
[d1_ph, d1_ph_time, d1_pl, d1_pl_time] = request.security(syminfo.tickerid, "D", [ta.pivothigh(high, swingBars, swingBars), time[swingBars], ta.pivotlow(low, swingBars, swingBars), time[swingBars]], lookahead=barmerge.lookahead_off)
// Track exactly when a new HTF pivot is formed to prevent processing the same pivot multiple times
var int last_h4_ph_t = 0
var int last_h4_pl_t = 0
var int last_d1_ph_t = 0
var int last_d1_pl_t = 0
h4_ph_new = not na(h4_ph) and h4_ph_time != last_h4_ph_t
h4_pl_new = not na(h4_pl) and h4_pl_time != last_h4_pl_t
d1_ph_new = not na(d1_ph) and d1_ph_time != last_d1_ph_t
d1_pl_new = not na(d1_pl) and d1_pl_time != last_d1_pl_t
if h4_ph_new
last_h4_ph_t := h4_ph_time
if h4_pl_new
last_h4_pl_t := h4_pl_time
if d1_ph_new
last_d1_ph_t := d1_ph_time
if d1_pl_new
last_d1_pl_t := d1_pl_time
if h4_ph_new or h4_pl_new
processPivots(h4_ph_new ? h4_ph : na, h4_ph_time, h4_pl_new ? h4_pl : na, h4_pl_time, h4_highs, h4_lows, h4Color, "H4")
if d1_ph_new or d1_pl_new
processPivots(d1_ph_new ? d1_ph : na, d1_ph_time, d1_pl_new ? d1_pl : na, d1_pl_time, d1_highs, d1_lows, d1Color, "D1")
// === SWEEP DETECTION & ALERTS (M15) ===
sweep_short = false
sweep_long = false
// Evaluate active EQH (Double Tops) - Iterating backwards allows safe removal
if array.size(activeEQH) > 0
for i = array.size(activeEQH) - 1 to 0
eqh = array.get(activeEQH, i)
// Bearish Sweep: Opened below, pierced above, closed below
if open < eqh and high > eqh and close < eqh
sweep_short := true
array.remove(activeEQH, i) // Remove level once swept to prevent duplicate executions
// Evaluate active EQL (Double Bottoms)
if array.size(activeEQL) > 0
for i = array.size(activeEQL) - 1 to 0
eql = array.get(activeEQL, i)
// Bullish Sweep: Opened above, pierced below, closed above
if open > eql and low < eql and close > eql
sweep_long := true
array.remove(activeEQL, i)
// Alert Triggers (Used for Webhook Auto-Execution)
if sweep_short and alertOnSweep
alert("LiqSweep Short: EQH Swept. Execute Sell Order.", alert.freq_once_per_bar_close)
if sweep_long and alertOnSweep
alert("LiqSweep Long: EQL Swept. Execute Buy Order.", alert.freq_once_per_bar_close)
// Visual Execution Markers
plotshape(sweep_short, title="Short Sweep Trigger", style=shape.triangledown, location=location.abovebar, color=color.red, size=size.small)
plotshape(sweep_long, title="Long Sweep Trigger", style=shape.triangleup, location=location.belowbar, color=color.green, size=size.small)