Trading bot grain (#33)
* Trading bot Grain * Fix a bit more of the trading bot * Advance on the tradingbot grain * Fix build * Fix db script * Fix user login * Fix a bit backtest * Fix cooldown and backtest * start fixing bot start * Fix startup * Setup local db * Fix build and update candles and scenario * Add bot registry * Add reminder * Updateing the grains * fix bootstraping * Save stats on tick * Save bot data every tick * Fix serialization * fix save bot stats * Fix get candles * use dict instead of list for position * Switch hashset to dict * Fix a bit * Fix bot launch and bot view * add migrations * Remove the tolist * Add agent grain * Save agent summary * clean * Add save bot * Update get bots * Add get bots * Fix stop/restart * fix Update config * Update scanner table on new backtest saved * Fix backtestRowDetails.tsx * Fix agentIndex * Update agentIndex * Fix more things * Update user cache * Fix * Fix account load/start/restart/run
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public ChandelierExitIndicatorBase(string name, int period, double multiplier) : base(name,
|
||||
IndicatorType.ChandelierExit)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
Period = period;
|
||||
Multiplier = multiplier;
|
||||
MinimumHistory = 1 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GetSignals(ChandelierType.Long, candles);
|
||||
GetSignals(ChandelierType.Short, candles);
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
ChandelierLong = candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Long).ToList(),
|
||||
ChandelierShort = candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Short).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private void GetSignals(ChandelierType chandelierType, HashSet<Candle> candles)
|
||||
{
|
||||
var chandelier = candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType)
|
||||
.Where(s => s.ChandelierExit.HasValue).ToList();
|
||||
var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory));
|
||||
var previousCandle = chandelierCandle[0];
|
||||
|
||||
foreach (var currentCandle in chandelierCandle.Skip(1))
|
||||
{
|
||||
// Short
|
||||
if (currentCandle.Close < previousCandle.ChandelierExit &&
|
||||
previousCandle.Close > previousCandle.ChandelierExit &&
|
||||
currentCandle.Close < previousCandle.Open &&
|
||||
chandelierType == ChandelierType.Short)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
// Long
|
||||
if (currentCandle.Close > previousCandle.ChandelierExit &&
|
||||
previousCandle.Close < previousCandle.ChandelierExit &&
|
||||
currentCandle.Close > currentCandle.Open &&
|
||||
chandelierType == ChandelierType.Long)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
|
||||
private List<CandleChandelier> MapChandelierToCandle(List<ChandelierResult> superTrend, IEnumerable<Candle> candles)
|
||||
{
|
||||
var superTrends = new List<CandleChandelier>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentChandelier = superTrend.Find(candle.Date);
|
||||
if (currentChandelier != null)
|
||||
{
|
||||
superTrends.Add(new CandleChandelier()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
ChandelierExit = (decimal)currentChandelier.ChandelierExit.Value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return superTrends;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleChandelier candleSignal, TradeDirection direction,
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class CandleChandelier : Candle
|
||||
{
|
||||
public decimal ChandelierExit { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public DualEmaCrossIndicatorBase(string name, int fastPeriod, int slowPeriod) : base(name,
|
||||
IndicatorType.DualEmaCross)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
FastPeriods = fastPeriod;
|
||||
SlowPeriods = slowPeriod;
|
||||
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
FastEma = candles.GetEma(FastPeriods.Value).ToList(),
|
||||
SlowEma = candles.GetEma(SlowPeriods.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fastEma = candles.GetEma(FastPeriods.Value).ToList();
|
||||
var slowEma = candles.GetEma(SlowPeriods.Value).ToList();
|
||||
|
||||
var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory));
|
||||
|
||||
if (dualEmaCandles.Count < 2)
|
||||
return null;
|
||||
|
||||
var previousCandle = dualEmaCandles[0];
|
||||
foreach (var currentCandle in dualEmaCandles.Skip(1))
|
||||
{
|
||||
// Short signal: Fast EMA crosses below Slow EMA
|
||||
if (previousCandle.FastEma > previousCandle.SlowEma &&
|
||||
currentCandle.FastEma < currentCandle.SlowEma)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
// Long signal: Fast EMA crosses above Slow EMA
|
||||
if (previousCandle.FastEma < previousCandle.SlowEma &&
|
||||
currentCandle.FastEma > currentCandle.SlowEma)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<CandleDualEma> MapDualEmaToCandle(List<EmaResult> fastEma, List<EmaResult> slowEma,
|
||||
IEnumerable<Candle> candles)
|
||||
{
|
||||
var dualEmaList = new List<CandleDualEma>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentFastEma = fastEma.Find(candle.Date);
|
||||
var currentSlowEma = slowEma.Find(candle.Date);
|
||||
|
||||
if (currentFastEma != null && currentFastEma.Ema.HasValue &&
|
||||
currentSlowEma != null && currentSlowEma.Ema.HasValue)
|
||||
{
|
||||
dualEmaList.Add(new CandleDualEma()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
FastEma = currentFastEma.Ema.Value,
|
||||
SlowEma = currentSlowEma.Ema.Value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dualEmaList;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
public class CandleDualEma : Candle
|
||||
{
|
||||
public double FastEma { get; set; }
|
||||
public double SlowEma { get; set; }
|
||||
}
|
||||
}
|
||||
79
src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs
Normal file
79
src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public EmaCrossIndicator(string name, int period) : base(name, IndicatorType.EmaCross)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
Ema = candles.GetEma(Period.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ema = candles.GetEma(Period.Value).ToList();
|
||||
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
|
||||
|
||||
if (ema.Count == 0)
|
||||
return null;
|
||||
|
||||
var previousCandle = emaCandles[0];
|
||||
foreach (var currentCandle in emaCandles.Skip(1))
|
||||
{
|
||||
if (previousCandle.Close > (decimal)currentCandle.Ema &&
|
||||
currentCandle.Close < (decimal)currentCandle.Ema)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
if (previousCandle.Close < (decimal)currentCandle.Ema &&
|
||||
currentCandle.Close > (decimal)currentCandle.Ema)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public EmaCrossIndicatorBase(string name, int period) : base(name, IndicatorType.EmaCross)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
Ema = candles.GetEma(Period.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ema = candles.GetEma(Period.Value).ToList();
|
||||
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet());
|
||||
|
||||
if (ema.Count == 0)
|
||||
return null;
|
||||
|
||||
var previousCandle = emaCandles[0];
|
||||
foreach (var currentCandle in emaCandles.Skip(1))
|
||||
{
|
||||
if (previousCandle.Close > (decimal)currentCandle.Ema &&
|
||||
currentCandle.Close < (decimal)currentCandle.Ema)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
if (previousCandle.Close < (decimal)currentCandle.Ema &&
|
||||
currentCandle.Close > (decimal)currentCandle.Ema)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
src/Managing.Domain/Indicators/Signals/LaggingSTC.cs
Normal file
146
src/Managing.Domain/Indicators/Signals/LaggingSTC.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
///<summary>
|
||||
/// Lagging STC Strategy: Combines Schaff Trend Cycle with volatility-based confirmation.
|
||||
/// Key Features:
|
||||
/// 1. Short signals on STC breakdown from overbught (75+ → ≤75) with recent compressed volatility (min >78)
|
||||
/// 2. Long signals on STC rebound from oversold (25- → ≥25) with recent compressed volatility (max <11)
|
||||
/// 3. Avoids look-ahead bias through proper rolling window implementation
|
||||
/// </summary>
|
||||
public class LaggingSTC : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public LaggingSTC(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name,
|
||||
IndicatorType.LaggingStc)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
FastPeriods = fastPeriods;
|
||||
SlowPeriods = slowPeriods;
|
||||
CyclePeriods = cyclePeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value * 3));
|
||||
|
||||
if (stcCandles.Count == 0)
|
||||
return null;
|
||||
|
||||
for (int i = 1; i < stcCandles.Count; i++)
|
||||
{
|
||||
var currentCandle = stcCandles[i];
|
||||
var previousCandle = stcCandles[i - 1];
|
||||
|
||||
/* VOLATILITY CONFIRMATION WINDOW
|
||||
* - 22-period rolling window (≈1 trading month)
|
||||
* - Ends at previous candle to avoid inclusion of current break
|
||||
* - Dynamic sizing for early dataset cases */
|
||||
// Calculate the lookback window ending at previousCandle (excludes currentCandle)
|
||||
int windowSize = 40;
|
||||
int windowStart = Math.Max(0, i - windowSize); // Ensure no negative indices
|
||||
var lookbackWindow = stcCandles
|
||||
.Skip(windowStart)
|
||||
.Take(i - windowStart) // Take up to previousCandle (i-1)
|
||||
.ToList();
|
||||
|
||||
double? minStc = lookbackWindow.Min(c => c.Stc);
|
||||
double? maxStc = lookbackWindow.Max(c => c.Stc);
|
||||
|
||||
// Short Signal: Break below 75 with prior min >78
|
||||
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
|
||||
{
|
||||
if (minStc > 78)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
}
|
||||
|
||||
// Long Signal: Break above 25 with prior max <11
|
||||
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
|
||||
{
|
||||
if (maxStc < 11)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
return new IndicatorsResultBase
|
||||
{
|
||||
Stc = stc
|
||||
};
|
||||
}
|
||||
|
||||
private List<CandleSct> MapStcToCandle(List<StcResult> stc, IEnumerable<Candle> candles)
|
||||
{
|
||||
var sctList = new List<CandleSct>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentSct = stc.Find(candle.Date);
|
||||
if (currentSct != null)
|
||||
{
|
||||
sctList.Add(new CandleSct()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
Stc = currentSct.Stc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sctList;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
private class CandleSct : Candle
|
||||
{
|
||||
public double? Stc { get; internal set; }
|
||||
}
|
||||
}
|
||||
122
src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs
Normal file
122
src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class MacdCrossIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public MacdCrossIndicatorBase(string name, int fastPeriods, int slowPeriods, int signalPeriods) :
|
||||
base(name, IndicatorType.MacdCross)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
FastPeriods = fastPeriods;
|
||||
SlowPeriods = slowPeriods;
|
||||
SignalPeriods = signalPeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var macd = candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList();
|
||||
var macdCandle = MapMacdToCandle(macd, candles.TakeLast(SignalPeriods.Value));
|
||||
|
||||
if (macd.Count == 0)
|
||||
return null;
|
||||
|
||||
var previousCandle = macdCandle[0];
|
||||
foreach (var currentCandle in macdCandle.Skip(1))
|
||||
{
|
||||
// // Only trigger signals when Signal line is outside -100 to 100 range (extreme conditions)
|
||||
// if (currentCandle.Signal < -200 || currentCandle.Signal > 200)
|
||||
// {
|
||||
//
|
||||
// }
|
||||
|
||||
// Check for MACD line crossing below Signal line (bearish cross)
|
||||
if (previousCandle.Macd > previousCandle.Signal && currentCandle.Macd < currentCandle.Signal)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
// Check for MACD line crossing above Signal line (bullish cross)
|
||||
if (previousCandle.Macd < previousCandle.Signal && currentCandle.Macd > currentCandle.Signal)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
Macd = candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private List<CandleMacd> MapMacdToCandle(List<MacdResult> macd, IEnumerable<Candle> candles)
|
||||
{
|
||||
var macdList = new List<CandleMacd>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentMacd = macd.Find(candle.Date);
|
||||
if (currentMacd != null)
|
||||
{
|
||||
macdList.Add(new CandleMacd()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
Macd = currentMacd.Macd.Value,
|
||||
Histogram = currentMacd.Histogram.Value,
|
||||
Signal = currentMacd.Signal.Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return macdList;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleMacd candleSignal, TradeDirection direction,
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
private class CandleMacd : Candle
|
||||
{
|
||||
public double Macd { get; set; }
|
||||
public double Signal { get; set; }
|
||||
public double Histogram { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
using Candle = Managing.Domain.Candles.Candle;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public RsiDivergenceConfirmIndicatorBase(string name, int period) : base(name, IndicatorType.RsiDivergenceConfirm)
|
||||
{
|
||||
Period = period;
|
||||
Signals = new List<LightSignal>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get RSI signals
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ticker = candles.First().Ticker;
|
||||
|
||||
try
|
||||
{
|
||||
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
|
||||
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
|
||||
|
||||
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
|
||||
return null;
|
||||
|
||||
GetLongSignals(candlesRsi, candles);
|
||||
GetShortSignals(candlesRsi, candles);
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
Rsi = candles.GetRsi(Period.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
var highPrices = new List<CandleRsi>();
|
||||
var lowPrices = new List<CandleRsi>();
|
||||
|
||||
var highRsi = new List<CandleRsi>();
|
||||
var lowRsi = new List<CandleRsi>();
|
||||
|
||||
highPrices.Add(firstCandleRsi);
|
||||
lowPrices.Add(firstCandleRsi);
|
||||
|
||||
highRsi.Add(firstCandleRsi);
|
||||
lowRsi.Add(firstCandleRsi);
|
||||
|
||||
var previousCandle = firstCandleRsi;
|
||||
|
||||
// For a long
|
||||
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
||||
{
|
||||
// If price go down
|
||||
if (previousCandle.Close > currentCandle.Close)
|
||||
{
|
||||
// because the last price is upper than the current
|
||||
highPrices.AddItem(previousCandle);
|
||||
|
||||
// Check if rsi is higher than the last lowest
|
||||
if (currentCandle.Rsi > lowRsi.TakeLast(Period.Value).Min(r => r.Rsi))
|
||||
{
|
||||
// If new higher high, we set it
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
if (currentCandle.Rsi > lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
// Price go down but RSI go up
|
||||
if (currentCandle.Close < lowPrices.TakeLast(Period.Value).Min(p => p.Close))
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.None);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No divergence, price go down, rsi go down
|
||||
lowRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
lowPrices.AddItem(currentCandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Price go up, so we have to update if price is a new higher high than previous candle
|
||||
// Normally always true
|
||||
if (previousCandle.Close < currentCandle.Close)
|
||||
highPrices.AddItem(currentCandle); //15-15-12-14-17
|
||||
|
||||
// If rsi is lower low or not set
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi || lowRsi.Last().Rsi == 0)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
// Price going up, so if its a new high we set it
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
CheckIfConfimation(currentCandle, TradeDirection.Long, candles);
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
|
||||
var signals = new List<Signal>();
|
||||
var highPrices = new List<CandleRsi>();
|
||||
var lowPrices = new List<CandleRsi>();
|
||||
|
||||
var highRsi = new List<CandleRsi>();
|
||||
var lowRsi = new List<CandleRsi>();
|
||||
|
||||
highPrices.Add(firstCandleRsi);
|
||||
lowPrices.Add(firstCandleRsi);
|
||||
|
||||
highRsi.Add(firstCandleRsi);
|
||||
lowRsi.Add(firstCandleRsi);
|
||||
|
||||
var previousCandle = firstCandleRsi;
|
||||
|
||||
// For a short
|
||||
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
||||
{
|
||||
// If price go up
|
||||
if (previousCandle.Close < currentCandle.Close)
|
||||
{
|
||||
// because the last price is lower than the current
|
||||
lowPrices.AddItem(previousCandle);
|
||||
|
||||
// Check if rsi is lower than the last high
|
||||
if (currentCandle.Rsi < highRsi.TakeLast(Period.Value).Max(r => r.Rsi))
|
||||
{
|
||||
// If new lower low, we set it
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
if (currentCandle.Rsi < highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
// Price go up but RSI go down
|
||||
if (currentCandle.Close > highPrices.TakeLast(Period.Value).Max(p => p.Close))
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.None);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No divergence, price go up, rsi go up
|
||||
highRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
highPrices.AddItem(currentCandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Price go down, so we have to update if price is a new lower low than previous candle
|
||||
if (previousCandle.Close > currentCandle.Close)
|
||||
lowPrices.AddItem(currentCandle);
|
||||
|
||||
// If rsi is higher high or not set
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi || highRsi.Last().Rsi == 0)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
// Price going down, so if its a new low we set it
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
CheckIfConfimation(currentCandle, TradeDirection.Short, candles);
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckIfConfimation(CandleRsi currentCandle, TradeDirection direction, HashSet<Candle> candles)
|
||||
{
|
||||
var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList();
|
||||
var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date
|
||||
&& s.Date < currentCandle.Date
|
||||
&& s.Direction == direction
|
||||
&& s.Confidence == Confidence.None
|
||||
&& s.Status != SignalStatus.Expired
|
||||
&& s.Status != SignalStatus.PositionOpen).ToList();
|
||||
|
||||
foreach (var signal in signalsOnPeriod)
|
||||
{
|
||||
if (direction == TradeDirection.Short && currentCandle.Close < signal.Candle.Open)
|
||||
{
|
||||
AddSignal(currentCandle, direction, Confidence.High);
|
||||
Signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
}
|
||||
|
||||
if (direction == TradeDirection.Long && currentCandle.Close > signal.Candle.Open)
|
||||
{
|
||||
AddSignal(currentCandle, direction, Confidence.High);
|
||||
Signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CandleRsi> MapRsiToCandle(IReadOnlyCollection<RsiResult> rsiResult,
|
||||
IEnumerable<Candle> candles)
|
||||
{
|
||||
return candles.Select(c => new CandleRsi()
|
||||
{
|
||||
Close = c.Close,
|
||||
Open = c.Open,
|
||||
Rsi = rsiResult.Find(c.Date).Rsi.GetValueOrDefault(),
|
||||
Date = c.Date,
|
||||
Ticker = c.Ticker,
|
||||
Exchange = c.Exchange
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private class CandleRsi : Candle
|
||||
{
|
||||
public double Rsi { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
using Candle = Managing.Domain.Candles.Candle;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
public TradeDirection Direction { get; set; }
|
||||
private const int UpperBand = 70;
|
||||
private const int LowerBand = 30;
|
||||
|
||||
public RsiDivergenceIndicatorBase(string name, int period) : base(name, IndicatorType.RsiDivergence)
|
||||
{
|
||||
Period = period;
|
||||
Signals = new List<LightSignal>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get RSI signals
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (!Period.HasValue || candles.Count <= Period)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ticker = candles.First().Ticker;
|
||||
|
||||
try
|
||||
{
|
||||
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
|
||||
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
|
||||
|
||||
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
|
||||
return null;
|
||||
|
||||
GetLongSignals(candlesRsi, candles);
|
||||
GetShortSignals(candlesRsi, candles);
|
||||
|
||||
return Signals;
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
Rsi = candles.GetRsi(Period.Value).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
var highPrices = new List<CandleRsi>();
|
||||
var lowPrices = new List<CandleRsi>();
|
||||
|
||||
var highRsi = new List<CandleRsi>();
|
||||
var lowRsi = new List<CandleRsi>();
|
||||
|
||||
highPrices.Add(firstCandleRsi);
|
||||
lowPrices.Add(firstCandleRsi);
|
||||
|
||||
highRsi.Add(firstCandleRsi);
|
||||
lowRsi.Add(firstCandleRsi);
|
||||
|
||||
var previousCandle = firstCandleRsi;
|
||||
|
||||
// For a long
|
||||
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
||||
{
|
||||
// If price go down
|
||||
if (previousCandle.Close > currentCandle.Close)
|
||||
{
|
||||
// because the last price is upper than the current
|
||||
highPrices.AddItem(previousCandle);
|
||||
|
||||
// Check if rsi is higher than the last lowest
|
||||
if (currentCandle.Rsi > lowRsi.TakeLast(Period.Value).Min(r => r.Rsi))
|
||||
{
|
||||
// If new higher high, we set it
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
if (currentCandle.Rsi > lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
// Price go down but RSI go up
|
||||
if (currentCandle.Close < lowPrices.TakeLast(Period.Value).Min(p => p.Close))
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, candles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No divergence, price go down, rsi go down
|
||||
lowRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
lowPrices.AddItem(currentCandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Price go up, so we have to update if price is a new higher high than previous candle
|
||||
// Normally always true
|
||||
if (previousCandle.Close < currentCandle.Close)
|
||||
highPrices.AddItem(currentCandle); //15-15-12-14-17
|
||||
|
||||
// If rsi is lower low or not set
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi || lowRsi.Last().Rsi == 0)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
// Price going up, so if its a new high we set it
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
|
||||
var signals = new List<Signal>();
|
||||
var highPrices = new List<CandleRsi>();
|
||||
var lowPrices = new List<CandleRsi>();
|
||||
|
||||
var highRsi = new List<CandleRsi>();
|
||||
var lowRsi = new List<CandleRsi>();
|
||||
|
||||
highPrices.Add(firstCandleRsi);
|
||||
lowPrices.Add(firstCandleRsi);
|
||||
|
||||
highRsi.Add(firstCandleRsi);
|
||||
lowRsi.Add(firstCandleRsi);
|
||||
|
||||
var previousCandle = firstCandleRsi;
|
||||
|
||||
// For a short
|
||||
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
||||
{
|
||||
// If price go up
|
||||
if (previousCandle.Close < currentCandle.Close)
|
||||
{
|
||||
// because the last price is lower than the current
|
||||
lowPrices.AddItem(previousCandle);
|
||||
|
||||
// Check if rsi is lower than the last high
|
||||
if (currentCandle.Rsi < highRsi.TakeLast(Period.Value).Max(r => r.Rsi))
|
||||
{
|
||||
// If new lower low, we set it
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
|
||||
if (currentCandle.Rsi < highRsi.Last().Rsi)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
// Price go up but RSI go down
|
||||
if (currentCandle.Close > highPrices.TakeLast(Period.Value).Max(p => p.Close))
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, candles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No divergence, price go up, rsi go up
|
||||
highRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
highPrices.AddItem(currentCandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Price go down, so we have to update if price is a new lower low than previous candle
|
||||
if (previousCandle.Close > currentCandle.Close)
|
||||
lowPrices.AddItem(currentCandle);
|
||||
|
||||
// If rsi is higher high or not set
|
||||
if (currentCandle.Rsi > highRsi.Last().Rsi || highRsi.Last().Rsi == 0)
|
||||
highRsi.AddItem(currentCandle);
|
||||
|
||||
// Price going down, so if its a new low we set it
|
||||
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
||||
lowRsi.AddItem(currentCandle);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet<Candle> candles)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, Confidence.Low,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
|
||||
if (Signals.Count(s => s.Identifier == signal.Identifier) < 1)
|
||||
{
|
||||
var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList();
|
||||
var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date).ToList();
|
||||
|
||||
if (signalsOnPeriod.Count == 1)
|
||||
signal.SetConfidence(Confidence.Medium);
|
||||
|
||||
if (signalsOnPeriod.Count >= 2)
|
||||
signal.SetConfidence(Confidence.High);
|
||||
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CandleRsi> MapRsiToCandle(IReadOnlyCollection<RsiResult> rsiResult,
|
||||
IEnumerable<Candle> candles)
|
||||
{
|
||||
return candles.Select(c => new CandleRsi()
|
||||
{
|
||||
Close = c.Close,
|
||||
Rsi = rsiResult.Find(c.Date).Rsi.GetValueOrDefault(),
|
||||
Date = c.Date,
|
||||
Ticker = c.Ticker,
|
||||
Exchange = c.Exchange
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private class CandleRsi : Candle
|
||||
{
|
||||
public double Rsi { get; set; }
|
||||
}
|
||||
}
|
||||
127
src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs
Normal file
127
src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class StcIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public StcIndicatorBase(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name,
|
||||
IndicatorType.Stc)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
FastPeriods = fastPeriods;
|
||||
SlowPeriods = slowPeriods;
|
||||
CyclePeriods = cyclePeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (FastPeriods != null)
|
||||
{
|
||||
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
if (CyclePeriods != null)
|
||||
{
|
||||
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value));
|
||||
|
||||
if (stc.Count == 0)
|
||||
return null;
|
||||
|
||||
var previousCandle = stcCandles[0];
|
||||
foreach (var currentCandle in stcCandles.Skip(1))
|
||||
{
|
||||
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
if (FastPeriods != null && SlowPeriods != null)
|
||||
{
|
||||
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
return new IndicatorsResultBase
|
||||
{
|
||||
Stc = stc
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<CandleSct> MapStcToCandle(List<StcResult> stc, IEnumerable<Candle> candles)
|
||||
{
|
||||
var sctList = new List<CandleSct>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentSct = stc.Find(candle.Date);
|
||||
if (currentSct != null)
|
||||
{
|
||||
sctList.Add(new CandleSct()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
Stc = currentSct.Stc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sctList;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
private class CandleSct : Candle
|
||||
{
|
||||
public double? Stc { get; internal set; }
|
||||
}
|
||||
}
|
||||
193
src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs
Normal file
193
src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class SuperTrendCrossEma : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public SuperTrendCrossEma(string name, int period, double multiplier) : base(name, IndicatorType.SuperTrendCrossEma)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
Period = period;
|
||||
Multiplier = multiplier;
|
||||
MinimumHistory = 100 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
// Validate sufficient historical data for all indicators
|
||||
const int emaPeriod = 50;
|
||||
const int adxPeriod = 14; // Standard ADX period
|
||||
const int adxThreshold = 25; // Minimum ADX level to confirm a trend
|
||||
|
||||
int minimumRequiredHistory = Math.Max(Math.Max(emaPeriod, adxPeriod), Period.Value * 2); // Ensure enough data
|
||||
if (candles.Count < minimumRequiredHistory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Calculate indicators
|
||||
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value)
|
||||
.Where(s => s.SuperTrend.HasValue)
|
||||
.ToList();
|
||||
|
||||
var ema50 = candles.GetEma(emaPeriod)
|
||||
.Where(e => e.Ema.HasValue)
|
||||
.ToList();
|
||||
|
||||
var adxResults = candles.GetAdx(adxPeriod)
|
||||
.Where(a => a.Adx.HasValue && a.Pdi.HasValue && a.Mdi.HasValue) // Ensure all values exist
|
||||
.ToList();
|
||||
|
||||
// 2. Create merged dataset with price + indicators
|
||||
var superTrendCandles = MapSuperTrendToCandle(superTrend, candles.TakeLast(minimumRequiredHistory));
|
||||
if (superTrendCandles.Count == 0)
|
||||
return null;
|
||||
|
||||
// 3. Add EMA50 and ADX values to the CandleSuperTrend objects
|
||||
foreach (var candle in superTrendCandles)
|
||||
{
|
||||
var emaValue = ema50.Find(e => e.Date == candle.Date)?.Ema;
|
||||
var adxValue = adxResults.Find(a => a.Date == candle.Date);
|
||||
|
||||
if (emaValue.HasValue)
|
||||
candle.Ema50 = emaValue.Value;
|
||||
|
||||
if (adxValue != null)
|
||||
{
|
||||
candle.Adx = (decimal)adxValue.Adx.Value;
|
||||
candle.Pdi = (decimal)adxValue.Pdi.Value;
|
||||
candle.Mdi = (decimal)adxValue.Mdi.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Signal detection logic with ADX filter
|
||||
for (int i = 1; i < superTrendCandles.Count; i++)
|
||||
{
|
||||
var current = superTrendCandles[i];
|
||||
var previous = superTrendCandles[i - 1];
|
||||
|
||||
// Convert SuperTrend to double for comparison
|
||||
double currentSuperTrend = (double)current.SuperTrend;
|
||||
double previousSuperTrend = (double)previous.SuperTrend;
|
||||
|
||||
// Ensure ADX data exists
|
||||
if (current.Adx < adxThreshold) // Only trade when ADX confirms trend strength
|
||||
continue;
|
||||
|
||||
/* LONG SIGNAL CONDITIONS:
|
||||
* 1. SuperTrend crosses above EMA50
|
||||
* 2. Price > SuperTrend and > EMA50
|
||||
* 3. Previous state shows SuperTrend < EMA50
|
||||
* 4. ADX > threshold and +DI > -DI (bullish momentum)
|
||||
*/
|
||||
bool longCross = currentSuperTrend > current.Ema50 &&
|
||||
previousSuperTrend < previous.Ema50;
|
||||
|
||||
bool longPricePosition = current.Close > (decimal)currentSuperTrend &&
|
||||
current.Close > (decimal)current.Ema50;
|
||||
|
||||
bool adxBullish = current.Pdi > current.Mdi; // Bullish momentum confirmation
|
||||
|
||||
if (longCross && longPricePosition && adxBullish)
|
||||
{
|
||||
AddSignal(current, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
|
||||
/* SHORT SIGNAL CONDITIONS:
|
||||
* 1. SuperTrend crosses below EMA50
|
||||
* 2. Price < SuperTrend and < EMA50
|
||||
* 3. Previous state shows SuperTrend > EMA50
|
||||
* 4. ADX > threshold and -DI > +DI (bearish momentum)
|
||||
*/
|
||||
bool shortCross = currentSuperTrend < current.Ema50 &&
|
||||
previousSuperTrend > previous.Ema50;
|
||||
|
||||
bool shortPricePosition = current.Close < (decimal)currentSuperTrend &&
|
||||
current.Close < (decimal)current.Ema50;
|
||||
|
||||
bool adxBearish = current.Mdi > current.Pdi; // Bearish momentum confirmation
|
||||
|
||||
if (shortCross && shortPricePosition && adxBearish)
|
||||
{
|
||||
AddSignal(current, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None)
|
||||
.OrderBy(s => s.Date)
|
||||
.ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<CandleSuperTrend> MapSuperTrendToCandle(List<SuperTrendResult> superTrend, IEnumerable<Candle> candles)
|
||||
{
|
||||
var superTrends = new List<CandleSuperTrend>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentSuperTrend = superTrend.Find(candle.Date);
|
||||
if (currentSuperTrend != null)
|
||||
{
|
||||
superTrends.Add(new CandleSuperTrend()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
SuperTrend = currentSuperTrend.SuperTrend.Value,
|
||||
LowerBand = currentSuperTrend.LowerBand,
|
||||
UpperBand = currentSuperTrend.UpperBand,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return superTrends;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
SuperTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date,
|
||||
candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class CandleSuperTrend : Candle
|
||||
{
|
||||
public decimal SuperTrend { get; internal set; }
|
||||
public decimal? LowerBand { get; internal set; }
|
||||
public decimal? UpperBand { get; internal set; }
|
||||
public double Ema50 { get; set; }
|
||||
public decimal Adx { get; set; } // ADX value
|
||||
public decimal Pdi { get; set; } // Positive Directional Indicator (+DI)
|
||||
public decimal Mdi { get; set; } // Negative Directional Indicator (-DI)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Skender.Stock.Indicators;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals;
|
||||
|
||||
public class SuperTrendIndicatorBase : IndicatorBase
|
||||
{
|
||||
public List<LightSignal> Signals { get; set; }
|
||||
|
||||
public SuperTrendIndicatorBase(string name, int period, double multiplier) : base(name, IndicatorType.SuperTrend)
|
||||
{
|
||||
Signals = new List<LightSignal>();
|
||||
Period = period;
|
||||
Multiplier = multiplier;
|
||||
MinimumHistory = 100 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue);
|
||||
var superTrendCandle = MapSuperTrendToCandle(superTrend, candles.TakeLast(MinimumHistory));
|
||||
|
||||
if (superTrendCandle.Count == 0)
|
||||
return null;
|
||||
|
||||
var previousCandle = superTrendCandle[0];
|
||||
foreach (var currentCandle in superTrendCandle.Skip(1))
|
||||
{
|
||||
// // Short
|
||||
// if (currentCandle.Close < previousCandle.SuperTrend && previousCandle.Close > previousCandle.SuperTrend)
|
||||
// {
|
||||
// AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
// }
|
||||
//
|
||||
// // Long
|
||||
// if (currentCandle.Close > previousCandle.SuperTrend && previousCandle.Close < previousCandle.SuperTrend)
|
||||
// {
|
||||
// AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
// }
|
||||
|
||||
if (currentCandle.SuperTrend < currentCandle.Close)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
|
||||
}
|
||||
else if (currentCandle.SuperTrend > currentCandle.Close)
|
||||
{
|
||||
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
|
||||
}
|
||||
|
||||
previousCandle = currentCandle;
|
||||
}
|
||||
|
||||
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
SuperTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private List<CandleSuperTrend> MapSuperTrendToCandle(IEnumerable<SuperTrendResult> superTrend,
|
||||
IEnumerable<Candle> candles)
|
||||
{
|
||||
var superTrends = new List<CandleSuperTrend>();
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var currentSuperTrend = superTrend.Find(candle.Date);
|
||||
if (currentSuperTrend != null)
|
||||
{
|
||||
superTrends.Add(new CandleSuperTrend()
|
||||
{
|
||||
Close = candle.Close,
|
||||
Open = candle.Open,
|
||||
Date = candle.Date,
|
||||
Ticker = candle.Ticker,
|
||||
Exchange = candle.Exchange,
|
||||
SuperTrend = currentSuperTrend.SuperTrend.Value,
|
||||
LowerBand = currentSuperTrend.LowerBand,
|
||||
UpperBand = currentSuperTrend.UpperBand,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return superTrends;
|
||||
}
|
||||
|
||||
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date,
|
||||
candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class CandleSuperTrend : Candle
|
||||
{
|
||||
public decimal SuperTrend { get; internal set; }
|
||||
public decimal? LowerBand { get; internal set; }
|
||||
public decimal? UpperBand { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Rules;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Strategies.Rules;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Strategies.Signals
|
||||
{
|
||||
public class ThreeWhiteSoldiersIndicatorBase : IndicatorBase
|
||||
{
|
||||
public ThreeWhiteSoldiersIndicatorBase(string name, int period)
|
||||
: base(name, IndicatorType.ThreeWhiteSoldiers)
|
||||
{
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public TradeDirection Direction { get; }
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
{
|
||||
var signals = new List<LightSignal>();
|
||||
|
||||
if (candles.Count <= 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var lastFourCandles = candles.TakeLast(4);
|
||||
Candle previousCandles = null;
|
||||
|
||||
foreach (var currentCandle in lastFourCandles)
|
||||
{
|
||||
if (Direction == TradeDirection.Long)
|
||||
{
|
||||
Check.That(new CloseHigherThanThePreviousHigh(previousCandles, currentCandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
Check.That(new CloseLowerThanThePreviousHigh(previousCandles, currentCandle));
|
||||
}
|
||||
|
||||
previousCandles = currentCandle;
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
catch (RuleException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user