Code: Select all
using System;
using System.IO;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.EasternStandardTime, AccessRights = AccessRights.File)]
public class ProSilverBullet : Robot
{
#region Parameters (Configuration)
[Parameter("Session Start (EST)", Group = "Session Constraints", DefaultValue = "10:00")]
public string SessionStartStr { get; set; }
[Parameter("Session End (EST)", Group = "Session Constraints", DefaultValue = "11:00")]
public string SessionEndStr { get; set; }
[Parameter("Max Spread (Pips)", Group = "Microstructure Filters", DefaultValue = 1.5, MinValue = 0.1, Step = 0.1)]
public double MaxSpreadPips { get; set; }
[Parameter("Risk / Reward Ratio", Group = "Execution Parameters", DefaultValue = 2.0, MinValue = 1.0, Step = 0.1)]
public double RiskReward { get; set; }
[Parameter("Risk % Per Trade", Group = "Execution Parameters", DefaultValue = 2.0, MinValue = 0.1, Step = 0.1)]
public double RiskPercent { get; set; }
[Parameter("Sweep Lookback (Bars)", Group = "Execution Parameters", DefaultValue = 10, MinValue = 3)]
public int SwingLength { get; set; }
[Parameter("Enable HTF Bias", Group = "Macro Filter", DefaultValue = true)]
public bool UseHtfFilter { get; set; }
[Parameter("HTF Timeframe", Group = "Macro Filter", DefaultValue = "Hour4")]
public TimeFrame HtfTimeframe { get; set; }
[Parameter("HTF EMA Length", Group = "Macro Filter", DefaultValue = 20, MinValue = 1)]
public int HtfEmaLength { get; set; }
#endregion
#region Private State & Services
private TimeSpan _sessionStart;
private TimeSpan _sessionEnd;
private SessionState _currentState;
private readonly string _botLabel = "PRO_SILVER_BULLET";
private Bars _htfBars;
private ExponentialMovingAverage _htfEma;
private enum SessionState
{
Offline,
Scanning,
TradeExecuted
}
#endregion
#region Initialization
protected override void OnStart()
{
if (!TimeSpan.TryParse(SessionStartStr, out _sessionStart) || !TimeSpan.TryParse(SessionEndStr, out _sessionEnd))
{
Print("🚨 CRITICAL: Invalid session time format. Use HH:MM.");
Stop();
return;
}
if (UseHtfFilter)
{
_htfBars = MarketData.GetBars(HtfTimeframe);
_htfEma = Indicators.ExponentialMovingAverage(_htfBars.ClosePrices, HtfEmaLength);
}
_currentState = SessionState.Offline;
}
#endregion
#region Core Evaluation Loop
protected override void OnBar()
{
UpdateSessionState();
// Guard clauses keep the execution thread clean
if (_currentState != SessionState.Scanning || IsPositionActive())
return;
if (IsSpreadTooWide())
return;
EvaluateSetup();
}
private void UpdateSessionState()
{
TimeSpan currentTime = Server.Time.TimeOfDay;
bool isInsideWindow = currentTime >= _sessionStart && currentTime < _sessionEnd;
if (isInsideWindow && _currentState == SessionState.Offline)
{
_currentState = SessionState.Scanning;
}
else if (!isInsideWindow && _currentState != SessionState.Offline)
{
_currentState = SessionState.Offline;
CancelPendingOrders();
}
}
#endregion
#region Market Structure Engine
private void EvaluateSetup()
{
if (Bars.Count < SwingLength + 3) return;
// 1. Evaluate Bias Provider
bool isBullishBias = true;
bool isBearishBias = true;
if (UseHtfFilter)
{
int htfIndex = _htfBars.ClosePrices.Count - 2; // Hard-lock to the last closed H4 candle
if (htfIndex >= 0)
{
double htfClose = _htfBars.ClosePrices[htfIndex];
double htfEmaValue = _htfEma.Result[htfIndex];
isBullishBias = htfClose > htfEmaValue;
isBearishBias = htfClose < htfEmaValue;
}
}
// 2. Liquidity Sweep Detection
double recentHigh = GetRecentHigh(SwingLength, 3);
double recentLow = GetRecentLow(SwingLength, 3);
bool sweptHigh = Bars.HighPrices.Last(1) >= recentHigh || Bars.HighPrices.Last(0) >= recentHigh;
bool sweptLow = Bars.LowPrices.Last(1) <= recentLow || Bars.LowPrices.Last(0) <= recentLow;
// 3. FVG Imbalance Detection
bool isBullishFvg = Bars.LowPrices.Last(0) > Bars.HighPrices.Last(2) && Bars.ClosePrices.Last(1) > Bars.OpenPrices.Last(1);
bool isBearishFvg = Bars.HighPrices.Last(0) < Bars.LowPrices.Last(2) && Bars.ClosePrices.Last(1) < Bars.OpenPrices.Last(1);
// 4. Signal Routing
if (sweptLow && isBullishFvg && isBullishBias)
{
double entryLimit = Bars.HighPrices.Last(2);
double stopLoss = Math.Min(Bars.LowPrices.Last(0), Math.Min(Bars.LowPrices.Last(1), Bars.LowPrices.Last(2)));
ExecuteOrder(TradeType.Buy, entryLimit, stopLoss);
}
else if (sweptHigh && isBearishFvg && isBearishBias)
{
double entryLimit = Bars.LowPrices.Last(2);
double stopLoss = Math.Max(Bars.HighPrices.Last(0), Math.Max(Bars.HighPrices.Last(1), Bars.HighPrices.Last(2)));
ExecuteOrder(TradeType.Sell, entryLimit, stopLoss);
}
}
private double GetRecentHigh(int lookback, int startIndex)
{
double max = double.MinValue;
for (int i = startIndex; i < startIndex + lookback; i++)
max = Math.Max(max, Bars.HighPrices.Last(i));
return max;
}
private double GetRecentLow(int lookback, int startIndex)
{
double min = double.MaxValue;
for (int i = startIndex; i < startIndex + lookback; i++)
min = Math.Min(min, Bars.LowPrices.Last(i));
return min;
}
#endregion
#region Execution & Risk Engine
private void ExecuteOrder(TradeType tradeType, double entryLimit, double stopLoss)
{
double riskInPrice = Math.Abs(entryLimit - stopLoss);
if (riskInPrice <= 0) return;
double riskInPips = riskInPrice / Symbol.PipSize;
double takeProfit = tradeType == TradeType.Buy
? entryLimit + (riskInPrice * RiskReward)
: entryLimit - (riskInPrice * RiskReward);
double tpInPips = Math.Abs(entryLimit - takeProfit) / Symbol.PipSize;
double volume = CalculateRiskAdjustedVolume(riskInPips);
if (volume <= 0) return;
// Server payload normalization
entryLimit = Symbol.NormalizePrice(entryLimit);
TradeResult result = PlaceLimitOrder(tradeType, SymbolName, volume, entryLimit, _botLabel, riskInPips, tpInPips);
if (result.IsSuccessful)
{
_currentState = SessionState.TradeExecuted;
Print($"✅ Setup secured. Type: {tradeType} | Vol: {volume} | Risk: {riskInPips:F1} pips");
}
else
{
LogRejectionTelemetry(tradeType, entryLimit, stopLoss, takeProfit, riskInPips, result.Error);
}
}
private double CalculateRiskAdjustedVolume(double riskInPips)
{
double riskAmount = Account.Balance * (RiskPercent / 100.0);
double exactVolume = (riskAmount / (riskInPips * Symbol.PipValue)) * Symbol.VolumeInUnitsMin;
return Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);
}
private bool IsSpreadTooWide()
{
double currentSpread = Symbol.Spread / Symbol.PipSize;
if (currentSpread > MaxSpreadPips)
{
Print($"⚠️ Spread spike: {currentSpread:F1} pips exceeds threshold of {MaxSpreadPips}. Skipping evaluation.");
return true;
}
return false;
}
#endregion
#region Telemetry & Logging
private void LogRejectionTelemetry(TradeType type, double entry, double sl, double tp, double slPips, ErrorCode? error)
{
if (error == null) return;
string action = $"{type.ToString().ToUpper()}_LIMIT";
double currentSpread = Symbol.Spread / Symbol.PipSize;
string logMsg = string.Format(
"[{0}] SYM: {1} | ACT: {2} | ENTRY: {3:F5} | SL: {4:F5} ({5:F1} pips) | TP: {6:F5} | SPREAD: {7:F1} pips | ERR: {8}",
Server.Time.ToString("yyyy-MM-dd HH:mm:ss"), SymbolName, action,
entry, sl, slPips, tp, currentSpread, error.ToString()
);
// cTrader log severity routing
PrintError($"❌ REJECTION: {logMsg}");
try
{
string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "cAlgo", "SilverBullet_Telemetry.csv");
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.AppendAllText(filePath, logMsg + Environment.NewLine);
}
catch (IOException ex)
{
PrintWarning($"Disk write failed: {ex.Message}");
}
}
#endregion
#region Active State Utilities
private bool IsPositionActive()
{
return Positions.Count(p => p.SymbolName == SymbolName && p.Label == _botLabel) > 0 ||
PendingOrders.Count(p => p.SymbolName == SymbolName && p.Label == _botLabel) > 0;
}
private void CancelPendingOrders()
{
foreach (var order in PendingOrders.Where(p => p.SymbolName == SymbolName && p.Label == _botLabel))
{
CancelPendingOrder(order);
}
}
#endregion
}
}