Debug an SL / TP manager for CTrader in C#

Spécifications

Hello

I've written an c# program for CTrader which watches for buy / sell stop orders. If it detects one the bot calculates the risk based on a eur amount I enter. The TP ratio is adjustable. 



E.g.. i place a pending order (doesnt matter at which price) with an SL. the bots immediately cancels it and makes a market order with the tp at the tp ratio and the SL and the providet position. Crucial: the lotsize is calculated based on the entered risk amount in the EA settings. Thats all, as simple as that.



what my problem is: It works only for forex. on gold and indices chatgpt didn't manage to code it even after hours. idk if my broker fusion is special but I can trade 0,01 NAS100 so it should work. Your job is to modify the bot so it runs perfectly on all stocks, indices, metals. And to add an input in which the risk is selectable as a percentage of the balance. Thats all.



to be considered done the TP / SL manager must work on all symbols including: BTC, Forex, Metals, Indices and Stocks and the Risk Percentage must be added. It should just behave like you would expect, nothing fancy.



Here is the code so far:



```



using cAlgo.API;

using cAlgo.API.Internals;

using System;

using System.Collections.Generic;



namespace cAlgo.Robots

{

    [Robot(AccessRights = AccessRights.None)]

    public class UniversalRiskPlacer : Robot

    {

        // -------- Benutzer-Parameter --------

        [Parameter("Risk (Account Ccy)", DefaultValue = 10.0, MinValue = 0.01, Step = 0.01)]

        public double RiskCurrency { get; set; }



        [Parameter("R (TP = R × SL)", DefaultValue = 5.00, MinValue = 0.5, Step = 0.01)]

        public double RMultiple { get; set; }



        [Parameter("Min Distance (pips)", DefaultValue = 5.0, MinValue = 0.0, Step = 0.1)]

        public double MinDistancePips { get; set; }



        [Parameter("Trigger: Pending Stop+SL", DefaultValue = true)]

        public bool TriggerOnPendingStopWithSL { get; set; }



        [Parameter("Require SL on Pending", DefaultValue = true)]

        public bool RequirePendingHasSL { get; set; }



        // --- Auto-Erkennung / Overrides (nur falls nötig) ---

        [Parameter("Auto Detect LotStep/MinLot", DefaultValue = true)]

        public bool AutoDetectLotStep { get; set; }



        [Parameter("Override MinLot (0 = auto)", DefaultValue = 0.00, MinValue = 0.0, Step = 0.001)]

        public double MinLotOverride { get; set; }



        [Parameter("Override LotStep (0 = auto)", DefaultValue = 0.00, MinValue = 0.0, Step = 0.001)]

        public double LotStepOverride { get; set; }



        [Parameter("Force Min Size if Below Risk", DefaultValue = false)]

        public bool ForceMinIfBelowRisk { get; set; }



        [Parameter("Dry Run (no orders)", DefaultValue = false)]

        public bool DryRun { get; set; }



        [Parameter("Debug Logs", DefaultValue = true)]

        public bool DebugLogs { get; set; }



        [Parameter("Debug Overlay", DefaultValue = true)]

        public bool DebugOverlay { get; set; }



        // -------- intern --------

        private readonly HashSet<long> _seen = new HashSet<long>();

        private readonly List<string> _ol = new List<string>();



        protected override void OnStart()

        {

            Timer.Start(1);

            Log($"[START] Risk={RiskCurrency} | R={RMultiple} | MinDist≥{MinDistancePips}p | AutoDetect={AutoDetectLotStep} | ForceMin={ForceMinIfBelowRisk} | DryRun={DryRun}");

        }



        protected override void OnTimer()

        {

            if (!TriggerOnPendingStopWithSL) return;



            // 1) Kandidaten einsammeln

            foreach (var po in PendingOrders)

            {

                if (_seen.Contains(po.Id)) continue;



                var pass = IsCandidate(po, out string reason);

                if (!pass)

                {

                    Log($"[GATE-1 SKIP] {po.SymbolName} #{po.Id}: {reason}");

                    continue;

                }



                _seen.Add(po.Id);

                try

                {

                    Handle(po);

                }

                catch (Exception ex)

                {

                    Log($"[ERR] Handle {po.SymbolName} #{po.Id}: {ex.Message}");

                }

            }

        }



        // ---------- Logic Gate 1: Kandidat? ----------

        private bool IsCandidate(PendingOrder po, out string reason)

        {

            reason = "";

            if (!TriggerOnPendingStopWithSL)

            {

                reason = "Trigger disabled";

                return false;

            }



            if (po.OrderType != PendingOrderType.Stop)

            {

                reason = $"OrderType={po.OrderType} (need Stop)";

                return false;

            }



            if (RequirePendingHasSL && !po.StopLoss.HasValue)

            {

                reason = "No SL on pending";

                return false;

            }



            var s = Symbols.GetSymbol(po.SymbolName);

            if (s == null)

            {

                reason = "Symbol null";

                return false;

            }



            double mkt = (po.TradeType == TradeType.Buy) ? s.Ask : s.Bid;

            double dPip = Math.Abs(mkt - po.TargetPrice) / s.PipSize;

            if (dPip < MinDistancePips)

            {

                reason = $"Dist {dPip:F2}p < MinDistance {MinDistancePips:F2}p";

                return false;

            }



            Log($"[GATE-1 OK] {po.SymbolName} #{po.Id}: dist={dPip:F2}p, SL={po.StopLoss.Value}");

            return true;

        }



        // ---------- Haupt-Handler mit mehrstufigen Gates ----------

        private void Handle(PendingOrder po)

        {

            var s    = Symbols.GetSymbol(po.SymbolName);

            var side = po.TradeType;

            double sl = po.StopLoss.Value;

            double entryMkt = (side == TradeType.Buy) ? s.Ask : s.Bid;



            // Gate 2: Symbolprofil

            var prof = BuildSymbolProfile(s);

            if (!prof.Valid)

            {

                Log($"[GATE-2 SKIP] {s.Name}: invalid pip/lot profile ({prof.Warn})");

                return;

            }

            Log($"[PROFILE] {s.Name} | units/lot≈{prof.UnitsPerLot:F4} | lotStep≈{prof.LotStep:F4} | minLot≈{prof.MinLot:F4} | pipVal/lot≈{prof.PipValuePerLot:F4} | uMin={prof.MinUnits} | uStep={prof.UnitStepBroker}");



            // Gate 3: Risiko-Mathe in LOTS (über VolumeForFixedRisk/AmountRisked)

            var risk = ComputeLots(s, entryMkt, sl, RiskCurrency);

            if (!risk.Ok)

            {

                Log($"[GATE-3 SKIP] {s.Name}: {risk.Msg}");

                return;

            }

            Log($"[RISK] pipDist={risk.PipDist:F2}p | lossPerLot={risk.LossPerLot:F4} | lotsRaw={risk.LotsRaw:F5}");



            // Gate 4: Lot-Rundung auf LotStep + MinLot

            double lotStep = (LotStepOverride > 0 && !AutoDetectLotStep) ? LotStepOverride : prof.LotStep;

            double minLot  = (MinLotOverride  > 0 && !AutoDetectLotStep) ? MinLotOverride  : prof.MinLot;



            if (!(lotStep > 0)) lotStep = 0.01;    // failsafe

            if (!(minLot > 0))  minLot  = lotStep; // failsafe



            double lotsRounded = Math.Floor(risk.LotsRaw / lotStep) * lotStep;

            double minRiskNeed = minLot * risk.LossPerLot; // € nötig für MinLot



            Log($"[ROUND] lotStep={lotStep:F4} | minLot={minLot:F4} | lotsRounded={lotsRounded:F5} | MinRiskNeeded={minRiskNeed:F2}");



            if (lotsRounded < minLot)

            {

                if (!ForceMinIfBelowRisk)

                {

                    Log($"[GATE-4 SKIP] {s.Name}: risk {RiskCurrency:F2} < {minRiskNeed:F2} for MinLot={minLot}");

                    return;

                }

                lotsRounded = minLot;

                Log($"[WARN] Forcing MinLot={minLot:F4} → tatsächliches Risiko ≈ {minRiskNeed:F2}.");

            }



            // Gate 5: Lots → Units, Step aus Profil

            long units = LotsToUnits(prof, lotsRounded);

            if (units <= 0)

            {

                Log($"[GATE-5 SKIP] {s.Name}: units<=0 nach Konvertierung (lots={lotsRounded:F5})");

                return;

            }

            Log($"[UNITS] lots={lotsRounded:F5} → units={units} | unitStep={prof.UnitStep:F0} | brokerMinU={prof.MinUnits}, brokerStepU={prof.UnitStepBroker}");



            // Gate 6: Broker-Min/Max clamp + Sanity-Lücke (Lot-Backcheck)

            if (prof.MinUnits > 0 && units < prof.MinUnits) units = prof.MinUnits;

            if (prof.MaxUnits > 0 && units > prof.MaxUnits) units = prof.MaxUnits;



            double backLots = UnitsToLots(prof, units);

            double backLotsDiff = Math.Abs(backLots - lotsRounded);

            if (backLotsDiff > lotStep + 1e-9)

            {

                Log($"[WARN] backLots diff {backLotsDiff:F4} > lotStep {lotStep:F4} (back={backLots:F4})");

            }



            // Gate 7: Orderausführung

            double rawTP = ComputeTP(entryMkt, sl, side, RMultiple);

            if (DryRun)

            {

                Log($"[DRY-RUN] {s.Name} {side}: units={units} | entry≈{entryMkt} | SL={sl} | TP≈{rawTP} | R={RMultiple:F2}");

                CancelPendingOrder(po);

                return;

            }



            CancelPendingOrder(po);

            var tr = ExecuteMarketOrder(side, s.Name, units, "UNIRISK");

            if (tr == null || !tr.IsSuccessful || tr.Position == null)

            {

                Log($"[FAIL] {s.Name} MarketOrder: {tr?.Error}");

                return;

            }



            var pos = tr.Position;

            var (nSL, nTP) = NormalizeSLTP(s, side, pos.EntryPrice, sl, rawTP);



#pragma warning disable CS0618

            ModifyPosition(pos, nSL, nTP);

#pragma warning restore CS0618



            Log($"[OK] {s.Name} {side} → units={units} | entry={pos.EntryPrice} | SL={nSL} | TP={nTP} | R={RMultiple:F2}");

        }



        // ---------- Symbolprofil (Auto-Detection + Failsafes) ----------

        private struct SymbolProfile

        {

            public bool Valid;

            public string Warn;



            public double UnitsPerLot;     // Units je 1.00 Lot

            public long   UnitStep;        // abgeleiteter Units-Schritt

            public long   UnitStepBroker;  // gemeldeter Broker Units-Step

            public long   MinUnits;

            public long   MaxUnits;



            public double LotStep;         // Lot-Schritt

            public double MinLot;          // Mindest-Lot



            public double PipValuePerLot;  // € pro Pip pro Lot (für Logging)

        }



        private SymbolProfile BuildSymbolProfile(Symbol s)

        {

            var p = new SymbolProfile

            {

                Valid = false,

                Warn  = ""

            };



            // Broker-Units-Infos

            p.MinUnits       = (long)Math.Round(Math.Max(1, s.VolumeInUnitsMin));

            p.MaxUnits       = (long)Math.Round(Math.Max(p.MinUnits, s.VolumeInUnitsMax));

            p.UnitStepBroker = (long)Math.Round(Math.Max(1, s.VolumeInUnitsStep));



            // Units pro 1.00 Lot

            double unitsPerLot = 0;

            try

            {

                unitsPerLot = s.QuantityToVolumeInUnits(1.0); // 1 Lot → Units

            }

            catch

            {

                unitsPerLot = 0;

            }



            if (!(unitsPerLot > 0))

            {

                unitsPerLot = s.LotSize > 0 ? s.LotSize : 100.0;

                p.Warn += "UnitsPerLot fallback; ";

            }



            p.UnitsPerLot = unitsPerLot;



            // LotStep/MinLot aus Units abgeleitet

            double lotStepFromUnits = 0.0;

            double minLotFromUnits  = 0.0;



            try

            {

                lotStepFromUnits = s.VolumeInUnitsToQuantity(s.VolumeInUnitsStep);

                minLotFromUnits  = s.VolumeInUnitsToQuantity(s.VolumeInUnitsMin);

            }

            catch

            {

                p.Warn += "VolumeInUnitsToQuantity failed; ";

            }



            if (AutoDetectLotStep)

            {

                p.LotStep = SnapLotStep(lotStepFromUnits);

                if (!(p.LotStep > 0)) p.LotStep = 0.01;



                double minLot = (minLotFromUnits > 0) ? minLotFromUnits : p.LotStep;

                p.MinLot = Math.Max(p.LotStep, minLot);

            }

            else

            {

                p.LotStep = (LotStepOverride > 0) ? LotStepOverride : 0.01;

                p.MinLot  = (MinLotOverride  > 0) ? MinLotOverride  : p.LotStep;

            }



            // Units-Schritt passend zum LotStep, aber nie kleiner als Broker-Step

            long stepUnitsFromLots = (long)Math.Round(p.UnitsPerLot * p.LotStep);

            if (stepUnitsFromLots <= 0) stepUnitsFromLots = 1;



            p.UnitStep = Math.Max(p.UnitStepBroker, stepUnitsFromLots);



            // PipValue je Lot (für Logging) aus AmountRisked: 1 Pip SL → Verlust pro Lot

            try

            {

                double volOneLot = s.QuantityToVolumeInUnits(1.0);

                p.PipValuePerLot = s.AmountRisked(volOneLot, 1.0);

            }

            catch

            {

                p.PipValuePerLot = double.NaN;

                p.Warn += "PipValuePerLot calc failed; ";

            }



            p.Valid = true;

            return p;

        }



        private static double SnapLotStep(double raw)

        {

            if (!(raw > 0)) return 0.01;

            // Snap auf 0.001 / 0.01 / 0.1 / 1

            double[] grid = { 0.001, 0.01, 0.1, 1.0 };

            double best = grid[0];

            double bd = Math.Abs(raw - best);

            foreach (var g in grid)

            {

                double d = Math.Abs(raw - g);

                if (d < bd)

                {

                    bd = d;

                    best = g;

                }

            }

            return best;

        }



        // ---------- Risiko in Lots (über VolumeForFixedRisk) ----------

        private struct RiskCalc

        {

            public bool Ok;

            public string Msg;

            public double PipDist;

            public double LossPerLot;

            public double LotsRaw;

        }



        private RiskCalc ComputeLots(Symbol s, double entry, double sl, double riskMoney)

        {

            var r = new RiskCalc { Ok = false, Msg = "unknown" };



            // Stop-Abstand in Pips

            double pipDist = Math.Abs(entry - sl) / s.PipSize;

            if (pipDist <= 0)

            {

                r.Msg = "pipDist<=0";

                return r;

            }



            // Volume in Units für das gewünschte Geld-Risiko

            double volRaw;

            try

            {

                volRaw = s.VolumeForFixedRisk(riskMoney, pipDist);

            }

            catch

            {

                r.Msg = "VolumeForFixedRisk failed";

                return r;

            }



            if (!(volRaw > 0))

            {

                r.Msg = "VolumeForFixedRisk<=0";

                return r;

            }



            // Lots aus Units

            double lots = s.VolumeInUnitsToQuantity(volRaw);



            // Verlust pro 1 Lot (für Logging / MinRiskNeeded)

            double volOneLot = s.QuantityToVolumeInUnits(1.0);

            double lossPerLot = s.AmountRisked(volOneLot, pipDist);



            if (!(lossPerLot > 0))

            {

                r.Msg = "lossPerLot<=0";

                return r;

            }



            r.Ok        = true;

            r.Msg       = "ok";

            r.PipDist   = pipDist;

            r.LossPerLot = lossPerLot;

            r.LotsRaw   = lots;

            return r;

        }



        // ---------- Lots ⇄ Units ----------

        private long LotsToUnits(SymbolProfile p, double lots)

        {

            double rawUnits = lots * p.UnitsPerLot;

            long units = (long)(Math.Floor(rawUnits / p.UnitStep) * p.UnitStep);

            return units;

        }



        private double UnitsToLots(SymbolProfile p, long units)

        {

            return units / p.UnitsPerLot;

        }



        // ---------- SL / TP ----------

        private static double ComputeTP(double entry, double sl, TradeType side, double r)

        {

            double d = Math.Abs(entry - sl);

            return side == TradeType.Buy ? entry + r * d : entry - r * d;

        }



        private static double RoundDownToTick(Symbol s, double price)

        {

            var steps = Math.Floor(price / s.TickSize);

            return steps * s.TickSize;

        }



        private static double RoundUpToTick(Symbol s, double price)

        {

            var steps = Math.Ceiling(price / s.TickSize);

            return steps * s.TickSize;

        }



        private (double nSL, double nTP) NormalizeSLTP(Symbol s, TradeType side, double entry, double rawSL, double rawTP)

        {

            double nSL, nTP;

            if (side == TradeType.Buy)

            {

                nSL = RoundDownToTick(s, Math.Min(rawSL, entry - s.TickSize));

                nTP = RoundUpToTick(s,   Math.Max(rawTP, entry + s.TickSize));

            }

            else

            {

                nSL = RoundUpToTick(s,   Math.Max(rawSL, entry + s.TickSize));

                nTP = RoundDownToTick(s, Math.Min(rawTP, entry - s.TickSize));

            }

            return (nSL, nTP);

        }



        // ---------- Pending kill ----------

        private void CancelPendingOrder(PendingOrder po)

        {

            try

            {

                var r = CancelPendingOrderAsync(po);

                Log($"[CANCEL] pending #{po.Id} sent");

            }

            catch (Exception ex)

            {

                Log($"[WARN] cancel pending #{po.Id}: {ex.Message}");

            }

        }



        // ---------- Logging ----------

        private void Log(string msg)

        {

            if (!DebugLogs && !DebugOverlay) return;

            string line = $"{Server.Time:HH:mm:ss} {msg}";

            if (DebugLogs) Print(line);



            if (DebugOverlay)

            {

                _ol.Add(line);

                if (_ol.Count > 16) _ol.RemoveAt(0);

                Chart.DrawStaticText("unirisk_dbg", string.Join("\n", _ol),

                    VerticalAlignment.Top, HorizontalAlignment.Left, Color.Yellow);

            }

        }

    }

}



```

Répondu

1
Développeur 1
Évaluation
(6)
Projets
6
17%
Arbitrage
1
0% / 0%
En retard
0
Travail
1
Développeur 1
Évaluation
(23)
Projets
29
14%
Arbitrage
3
0% / 67%
En retard
3
10%
Travail
2
Développeur 2
Évaluation
(294)
Projets
470
39%
Arbitrage
102
40% / 24%
En retard
78
17%
Occupé
Publié : 2 codes
3
Développeur 3
Évaluation
(5)
Projets
6
0%
Arbitrage
0
En retard
0
Travail
4
Développeur 4
Évaluation
(27)
Projets
38
24%
Arbitrage
14
0% / 93%
En retard
4
11%
Gratuit
5
Développeur 5
Évaluation
(1)
Projets
0
0%
Arbitrage
1
0% / 100%
En retard
0
Gratuit
Commandes similaires
I need an experienced MQL5 developer to convert my complete Pine Script trading strategy into a fully functional MQL5 Expert Advisor. Strategy Overview: MACD crossover signals with EMA200 trend filter Multi-timeframe analysis (current TF + 1H, 2H, 4H, Daily, Weekly) Choppiness Index filter (avoids ranging markets) Consistent trend verification across multiple lookback periods ATR-based stop loss and take profit with
Ich benötige einen Programmierer der mir einen EA für MT4 erstellt aber auch Kenntnisse in MT5 hat und mir diesen EA später auch in MT5 umschreiben kann. Der EA muss an festgelegten Hochpunkten oder Tiefpunkten die durch einen fraktalen Indikator ermittelt werden eine Position eröffnen, sobald der Hochpunkt oder Tiefpunkt erreicht wird. Die Positionseröffnung erfolgt in einem kleineren Timeframe mittels festgelegter

Informations sur le projet

Budget
43+ USD