Add new strat DualEmaCross

This commit is contained in:
2025-06-02 21:28:56 +07:00
parent de9f77d5ba
commit 7fce1fa59e
22 changed files with 264 additions and 62 deletions

View File

@@ -1,6 +1,8 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Signals;
using Managing.Domain.Strategies.Trends;
using Xunit;
using static Managing.Common.Enums;
@@ -22,7 +24,7 @@ namespace Managing.Application.Tests
{
var account = GetAccount(exchange);
// Arrange
var rsiStrategy = new RSIDivergenceStrategy("unittest", 5);
var rsiStrategy = new RsiDivergenceStrategy("unittest", 5);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
var resultSignal = new List<Signal>();
@@ -56,7 +58,7 @@ namespace Managing.Application.Tests
{
// Arrange
var account = GetAccount(exchange);
var rsiStrategy = new RSIDivergenceStrategy("unittest", 5);
var rsiStrategy = new RsiDivergenceStrategy("unittest", 5);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
var resultSignal = new List<Signal>();

View File

@@ -1,5 +1,5 @@
using Managing.Domain.Candles;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Signals;
using Managing.Domain.Workflows;
using Newtonsoft.Json;
using static Managing.Common.Enums;
@@ -30,7 +30,7 @@ public class RsiDiv : FlowBase
MapParameters();
var candles = JsonConvert.DeserializeObject<HashSet<Candle>>(input);
var strategy = new RSIDivergenceStrategy(Name, RsiDivParameters.Period);
var strategy = new RsiDivergenceStrategy(Name, RsiDivParameters.Period);
strategy.UpdateCandles(candles);
strategy.Run();

View File

@@ -63,7 +63,8 @@ public static class Enums
Stc,
StDev,
LaggingStc,
SuperTrendCrossEma
SuperTrendCrossEma,
DualEmaCross
}
public enum SignalType

View File

@@ -1,6 +1,9 @@
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Candles;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Context;
using Managing.Domain.Strategies.Signals;
using Managing.Domain.Strategies.Trends;
using static Managing.Common.Enums;
namespace Managing.Domain.Scenarios;
@@ -24,13 +27,15 @@ public static class ScenarioHelpers
IStrategy result = strategy.Type switch
{
StrategyType.StDev => new StDevContext(strategy.Name, strategy.Period.Value),
StrategyType.RsiDivergence => new RSIDivergenceStrategy(strategy.Name,
StrategyType.RsiDivergence => new RsiDivergenceStrategy(strategy.Name,
strategy.Period.Value),
StrategyType.RsiDivergenceConfirm => new RSIDivergenceConfirmStrategy(strategy.Name,
StrategyType.RsiDivergenceConfirm => new RsiDivergenceConfirmStrategy(strategy.Name,
strategy.Period.Value),
StrategyType.MacdCross => new MacdCrossStrategy(strategy.Name,
strategy.FastPeriods.Value, strategy.SlowPeriods.Value, strategy.SignalPeriods.Value),
StrategyType.EmaCross => new EmaCrossStrategy(strategy.Name, strategy.Period.Value),
StrategyType.DualEmaCross => new DualEmaCrossStrategy(strategy.Name,
strategy.FastPeriods.Value, strategy.SlowPeriods.Value),
StrategyType.ThreeWhiteSoldiers => new ThreeWhiteSoldiersStrategy(strategy.Name,
strategy.Period.Value),
StrategyType.SuperTrend => new SuperTrendStrategy(strategy.Name,
@@ -41,7 +46,7 @@ public static class ScenarioHelpers
StrategyType.StochRsiTrend => new StochRsiTrendStrategy(strategy.Name,
strategy.Period.Value, strategy.StochPeriods.Value, strategy.SignalPeriods.Value,
strategy.SmoothPeriods.Value),
StrategyType.Stc => new STCStrategy(strategy.Name, strategy.CyclePeriods.Value,
StrategyType.Stc => new StcStrategy(strategy.Name, strategy.CyclePeriods.Value,
strategy.FastPeriods.Value, strategy.SlowPeriods.Value),
StrategyType.LaggingStc => new LaggingSTC(strategy.Name, strategy.CyclePeriods.Value,
strategy.FastPeriods.Value, strategy.SlowPeriods.Value),
@@ -99,6 +104,18 @@ public static class ScenarioHelpers
}
break;
case StrategyType.DualEmaCross:
if (!fastPeriods.HasValue || !slowPeriods.HasValue)
{
throw new Exception(
$"Missing fastPeriods or slowPeriods for {strategy.Type} strategy type");
}
else
{
strategy.FastPeriods = fastPeriods;
strategy.SlowPeriods = slowPeriods;
}
break;
case StrategyType.ThreeWhiteSoldiers:
break;
@@ -164,6 +181,7 @@ public static class ScenarioHelpers
StrategyType.RsiDivergenceConfirm => SignalType.Signal,
StrategyType.MacdCross => SignalType.Signal,
StrategyType.EmaCross => SignalType.Signal,
StrategyType.DualEmaCross => SignalType.Signal,
StrategyType.ThreeWhiteSoldiers => SignalType.Signal,
StrategyType.SuperTrend => SignalType.Signal,
StrategyType.ChandelierExit => SignalType.Signal,

View File

@@ -60,13 +60,13 @@ public static class TradingBox
{
var signalOnCandles = new HashSet<Signal>();
var limitedCandles = newCandles.ToList().TakeLast(600).ToList();
foreach (var strategy in strategies)
{
strategy.UpdateCandles(limitedCandles.ToHashSet());
var signals = strategy.Run();
if (signals == null || signals.Count == 0)
if (signals == null || signals.Count == 0)
{
// For trend and context strategies, lack of signal might be meaningful
// Signal strategies are expected to be sparse, so we continue
@@ -74,7 +74,7 @@ public static class TradingBox
{
continue;
}
// For trend strategies, no signal might mean neutral trend
// For context strategies, no signal might mean no restrictions
// We'll let the ComputeSignals method handle these cases
@@ -82,6 +82,7 @@ public static class TradingBox
}
// Ensure limitedCandles is ordered chronologically
loopbackPeriod = 20;
var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList();
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
var candleLoopback = orderedCandles.TakeLast(loopback).ToList();
@@ -135,6 +136,13 @@ public static class TradingBox
return signalOnCandles.Single();
}
// Check if all strategies produced signals - this is required for composite signals
if (signalOnCandles.Count != strategies.Count)
{
// Not all strategies produced signals - composite signal requires all strategies to contribute
return null;
}
// Group signals by type for analysis
var signalStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
var trendStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
@@ -153,7 +161,8 @@ public static class TradingBox
var signalDirection = EvaluateSignalDirection(signalStrategies, config);
// Determine final direction and confidence
var (finalDirection, confidence) = DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config);
var (finalDirection, confidence) =
DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config);
if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence)
{
@@ -161,8 +170,9 @@ public static class TradingBox
}
// Create composite signal
var lastSignal = signalStrategies.LastOrDefault() ?? trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault();
var lastSignal = signalStrategies.LastOrDefault() ??
trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault();
return new Signal(
ticker,
finalDirection,
@@ -170,17 +180,18 @@ public static class TradingBox
lastSignal?.Candle,
lastSignal?.Date ?? DateTime.UtcNow,
lastSignal?.Exchange ?? config.DefaultExchange,
StrategyType.Composite,
StrategyType.Composite,
SignalType.Signal);
}
/// <summary>
/// Validates context strategies based on confidence levels indicating market condition quality
/// </summary>
private static bool ValidateContextStrategies(HashSet<IStrategy> allStrategies, List<Signal> contextSignals, StrategyComboConfig config)
private static bool ValidateContextStrategies(HashSet<IStrategy> allStrategies, List<Signal> contextSignals,
StrategyComboConfig config)
{
var contextStrategiesCount = allStrategies.Count(s => s.SignalType == SignalType.Context);
if (contextStrategiesCount == 0)
{
return true; // No context strategies, no restrictions
@@ -256,9 +267,9 @@ public static class TradingBox
/// Determines final signal direction and confidence based on signal and trend analysis
/// </summary>
private static (TradeDirection Direction, Confidence Confidence) DetermineFinalSignal(
TradeDirection signalDirection,
TradeDirection trendDirection,
List<Signal> signalStrategies,
TradeDirection signalDirection,
TradeDirection trendDirection,
List<Signal> signalStrategies,
List<Signal> trendStrategies,
StrategyComboConfig config)
{
@@ -294,12 +305,12 @@ public static class TradingBox
{
// Calculate confidence based on trend strength
var totalTrend = trendStrategies.Count;
var majorityDirection = trendDirection == TradeDirection.Long
var majorityDirection = trendDirection == TradeDirection.Long
? trendStrategies.Count(s => s.Direction == TradeDirection.Long)
: trendStrategies.Count(s => s.Direction == TradeDirection.Short);
var agreementPercentage = (decimal)majorityDirection / totalTrend;
if (agreementPercentage >= 0.8m)
return (trendDirection, Confidence.High);
else if (agreementPercentage >= config.TrendStrongAgreementThreshold)

View File

@@ -5,6 +5,8 @@ namespace Managing.Domain.Strategies.Base;
public class StrategiesResultBase
{
public List<EmaResult> Ema { get; set; }
public List<EmaResult> FastEma { get; set; }
public List<EmaResult> SlowEma { get; set; }
public List<MacdResult> Macd { get; set; }
public List<RsiResult> Rsi { get; set; }
public List<StochResult> Stoch { get; set; }

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Context;
public class StDevContext : Strategy
{
@@ -39,7 +39,7 @@ public class StDevContext : Strategy
// Lower absolute Z-score = more normal volatility = higher confidence for trading
// Higher absolute Z-score = more extreme volatility = lower confidence for trading
Confidence confidence;
if (Math.Abs(zScore) <= 0.5)
{
// Very low volatility - ideal conditions for trading

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class ChandelierExitStrategy : Strategy
{

View File

@@ -0,0 +1,119 @@
using Managing.Core;
using Managing.Domain.Candles;
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 DualEmaCrossStrategy : EmaBaseStrategy
{
public List<Signal> Signals { get; set; }
public DualEmaCrossStrategy(string name, int fastPeriod, int slowPeriod) : base(name, StrategyType.DualEmaCross)
{
Signals = new List<Signal>();
FastPeriods = fastPeriod;
SlowPeriods = slowPeriod;
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
}
public override StrategiesResultBase GetStrategyValues()
{
return new StrategiesResultBase()
{
FastEma = Candles.GetEma(FastPeriods.Value).ToList(),
SlowEma = Candles.GetEma(SlowPeriods.Value).ToList()
};
}
public override List<Signal> Run()
{
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 Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
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; }
}
}

View File

@@ -4,7 +4,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class EmaCrossStrategy : EmaBaseStrategy
{

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
///<summary>
/// Lagging STC Strategy: Combines Schaff Trend Cycle with volatility-based confirmation.

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class MacdCrossStrategy : Strategy
{

View File

@@ -5,13 +5,13 @@ using Skender.Stock.Indicators;
using static Managing.Common.Enums;
using Candle = Managing.Domain.Candles.Candle;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class RSIDivergenceConfirmStrategy : Strategy
public class RsiDivergenceConfirmStrategy : Strategy
{
public List<Signal> Signals { get; set; }
public RSIDivergenceConfirmStrategy(string name, int period) : base(name, StrategyType.RsiDivergenceConfirm)
public RsiDivergenceConfirmStrategy(string name, int period) : base(name, StrategyType.RsiDivergenceConfirm)
{
Period = period;
Signals = new List<Signal>();

View File

@@ -5,16 +5,16 @@ using Skender.Stock.Indicators;
using static Managing.Common.Enums;
using Candle = Managing.Domain.Candles.Candle;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class RSIDivergenceStrategy : Strategy
public class RsiDivergenceStrategy : Strategy
{
public List<Signal> Signals { get; set; }
public TradeDirection Direction { get; set; }
private const int UpperBand = 70;
private const int LowerBand = 30;
public RSIDivergenceStrategy(string name, int period) : base(name, StrategyType.RsiDivergence)
public RsiDivergenceStrategy(string name, int period) : base(name, StrategyType.RsiDivergence)
{
Period = period;
Signals = new List<Signal>();

View File

@@ -5,13 +5,13 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class STCStrategy : Strategy
public class StcStrategy : Strategy
{
public List<Signal> Signals { get; set; }
public STCStrategy(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name, StrategyType.Stc)
public StcStrategy(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name, StrategyType.Stc)
{
Signals = new List<Signal>();
FastPeriods = fastPeriods;
@@ -28,26 +28,32 @@ public class STCStrategy : Strategy
try
{
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
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 (FastPeriods != null)
{
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
if (CyclePeriods != null)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
var stcCandles = MapStcToCandle(stc, Candles.TakeLast(CyclePeriods.Value));
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
if (stc.Count == 0)
return null;
previousCandle = currentCandle;
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();
@@ -60,11 +66,16 @@ public class STCStrategy : Strategy
public override StrategiesResultBase GetStrategyValues()
{
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
return new StrategiesResultBase
if (FastPeriods != null && SlowPeriods != null)
{
Stc = stc
};
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
return new StrategiesResultBase
{
Stc = stc
};
}
return null;
}
private List<CandleSct> MapStcToCandle(List<StcResult> stc, IEnumerable<Candle> candles)

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class SuperTrendCrossEma : Strategy
{

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Signals;
public class SuperTrendStrategy : Strategy
{

View File

@@ -4,7 +4,7 @@ using Managing.Domain.Strategies.Base;
using Managing.Domain.Strategies.Rules;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies
namespace Managing.Domain.Strategies.Signals
{
public class ThreeWhiteSoldiersStrategy : Strategy
{

View File

@@ -4,7 +4,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Trends;
public class EmaTrendStrategy : EmaBaseStrategy
{

View File

@@ -5,7 +5,7 @@ using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
namespace Managing.Domain.Strategies.Trends;
public class StochRsiTrendStrategy : Strategy
{

View File

@@ -2991,6 +2991,7 @@ export enum StrategyType {
StDev = "StDev",
LaggingStc = "LaggingStc",
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
}
export enum SignalType {
@@ -3017,6 +3018,8 @@ export interface KeyValuePairOfDateTimeAndDecimal {
export interface StrategiesResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;

View File

@@ -227,6 +227,41 @@ const StrategyList: React.FC = () => {
</>
) : null}
{strategyType == StrategyType.DualEmaCross ? (
<>
<div className="form-control">
<div className="input-group">
<label htmlFor="period" className="label mr-6">
Fast Periods
</label>
<label className="input-group">
<input
type="number"
placeholder="9"
className="input"
{...register('fastPeriods')}
/>
</label>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="period" className="label mr-6">
Slow Periods
</label>
<label className="input-group">
<input
type="number"
placeholder="21"
className="input"
{...register('slowPeriods')}
/>
</label>
</div>
</div>
</>
) : null}
{strategyType == StrategyType.Stc || strategyType == StrategyType.LaggingStc ? (
<>
<div className="form-control">