Skip to content

Commit 38ba53a

Browse files
authored
Merge pull request StockSharp#664 from StockSharp/claude/debug-algo-candles-011CUM4GWz6rBQ4gkge45f2J
Debug Algo/Candles Trading Algorithm
2 parents f05657b + 6b6139c commit 38ba53a

3 files changed

Lines changed: 260 additions & 8 deletions

File tree

Algo/Candles/CandleHelper.cs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,13 @@ public static CandlePriceLevel PoC(this VolumeProfileBuilder volumeProfile)
278278
if (volumeProfile == null)
279279
throw new ArgumentNullException(nameof(volumeProfile));
280280

281-
var max = volumeProfile.PriceLevels.Select(p => p.BuyVolume + p.SellVolume).Max();
282-
return volumeProfile.PriceLevels.FirstOrDefault(p => p.BuyVolume + p.SellVolume == max);
281+
var levels = volumeProfile.PriceLevels.ToArray();
282+
283+
if (levels.Length == 0)
284+
return default;
285+
286+
var max = levels.Select(p => p.BuyVolume + p.SellVolume).Max();
287+
return levels.FirstOrDefault(p => p.BuyVolume + p.SellVolume == max);
283288
}
284289

285290
/// <summary>
@@ -290,6 +295,10 @@ public static CandlePriceLevel PoC(this VolumeProfileBuilder volumeProfile)
290295
public static decimal BuyVolAbovePoC(this VolumeProfileBuilder volumeProfile)
291296
{
292297
var poc = volumeProfile.PoC();
298+
299+
if (poc.Price == 0)
300+
return 0;
301+
293302
return volumeProfile.PriceLevels.Where(p => p.Price > poc.Price).Select(p => p.BuyVolume).Sum();
294303
}
295304

@@ -301,6 +310,10 @@ public static decimal BuyVolAbovePoC(this VolumeProfileBuilder volumeProfile)
301310
public static decimal BuyVolBelowPoC(this VolumeProfileBuilder volumeProfile)
302311
{
303312
var poc = volumeProfile.PoC();
313+
314+
if (poc.Price == 0)
315+
return 0;
316+
304317
return volumeProfile.PriceLevels.Where(p => p.Price < poc.Price).Select(p => p.BuyVolume).Sum();
305318
}
306319

@@ -312,6 +325,10 @@ public static decimal BuyVolBelowPoC(this VolumeProfileBuilder volumeProfile)
312325
public static decimal SellVolAbovePoC(this VolumeProfileBuilder volumeProfile)
313326
{
314327
var poc = volumeProfile.PoC();
328+
329+
if (poc.Price == 0)
330+
return 0;
331+
315332
return volumeProfile.PriceLevels.Where(p => p.Price > poc.Price).Select(p => p.SellVolume).Sum();
316333
}
317334

@@ -323,6 +340,10 @@ public static decimal SellVolAbovePoC(this VolumeProfileBuilder volumeProfile)
323340
public static decimal SellVolBelowPoC(this VolumeProfileBuilder volumeProfile)
324341
{
325342
var poc = volumeProfile.PoC();
343+
344+
if (poc.Price == 0)
345+
return 0;
346+
326347
return volumeProfile.PriceLevels.Where(p => p.Price < poc.Price).Select(p => p.SellVolume).Sum();
327348
}
328349

@@ -366,8 +387,13 @@ public static CandlePriceLevel PriceLevelOfMaxDelta(this VolumeProfileBuilder vo
366387
if (volumeProfile == null)
367388
throw new ArgumentNullException(nameof(volumeProfile));
368389

369-
var delta = volumeProfile.PriceLevels.Select(p => p.BuyVolume - p.SellVolume).Max();
370-
return volumeProfile.PriceLevels.FirstOrDefault(p => p.BuyVolume - p.SellVolume == delta);
390+
var levels = volumeProfile.PriceLevels.ToArray();
391+
392+
if (levels.Length == 0)
393+
return default;
394+
395+
var delta = levels.Select(p => p.BuyVolume - p.SellVolume).Max();
396+
return levels.FirstOrDefault(p => p.BuyVolume - p.SellVolume == delta);
371397
}
372398

373399
/// <summary>
@@ -380,8 +406,13 @@ public static CandlePriceLevel PriceLevelOfMinDelta(this VolumeProfileBuilder vo
380406
if (volumeProfile == null)
381407
throw new ArgumentNullException(nameof(volumeProfile));
382408

383-
var delta = volumeProfile.PriceLevels.Select(p => p.BuyVolume - p.SellVolume).Min();
384-
return volumeProfile.PriceLevels.FirstOrDefault(p => p.BuyVolume - p.SellVolume == delta);
409+
var levels = volumeProfile.PriceLevels.ToArray();
410+
411+
if (levels.Length == 0)
412+
return default;
413+
414+
var delta = levels.Select(p => p.BuyVolume - p.SellVolume).Min();
415+
return levels.FirstOrDefault(p => p.BuyVolume - p.SellVolume == delta);
385416
}
386417

387418
/// <summary>
@@ -518,7 +549,7 @@ public static void ConvertToTrades(this ICandleMessage candleMsg, decimal volume
518549
candleMsg.OpenPrice == candleMsg.LowPrice ||
519550
candleMsg.TotalVolume == 1)
520551
{
521-
// âñå öåíû â ñâå÷å ðàâíû èëè îáúåì ðàâåí 1 - ñ÷èòàåì åå çà îäèí òèê
552+
// For candle with single price or volume equals 1 - generate only one tick
522553
ticks[0] = (Sides.Buy, candleMsg.OpenPrice, candleMsg.TotalVolume, time);
523554

524555
ticks[1] = ticks[2] = ticks[3] = default;

Algo/Candles/Compression/VolumeProfileBuilder.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public void Calculate()
116116
{
117117
if (_levels.Count == 0)
118118
return;
119-
119+
120120
PoC = default;
121121
High = default;
122122
Low = default;
@@ -126,6 +126,9 @@ public void Calculate()
126126

127127
PoC = _levels.FirstOrDefault(p => p.BuyVolume + p.SellVolume == currVolume);
128128

129+
if (PoC.Price == 0)
130+
return;
131+
129132
var abovePoc = Combine(_levels.Where(p => p.Price > PoC.Price).OrderBy(p => p.Price), true);
130133
var belowePoc = Combine(_levels.Where(p => p.Price < PoC.Price).OrderByDescending(p => p.Price), false);
131134

Tests/CandleTests.cs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace StockSharp.Tests;
22

3+
using StockSharp.Algo.Candles;
34
using StockSharp.Algo.Candles.Compression;
45

56
[TestClass]
@@ -872,4 +873,221 @@ public void VolumeProfile_MultipleBelowPairs()
872873
builder.Low.Price.AssertEqual(96m);
873874
builder.High.Price.AssertEqual(100m);
874875
}
876+
877+
[TestMethod]
878+
public void VolumeProfile_EmptyBuilder_CalculateDoesNotCrash()
879+
{
880+
// Test that Calculate() on empty builder doesn't crash with InvalidOperationException
881+
var builder = new VolumeProfileBuilder();
882+
883+
// Should not throw InvalidOperationException from Max() on empty sequence
884+
builder.Calculate();
885+
886+
// Should return default values
887+
builder.PoC.Price.AssertEqual(0m);
888+
builder.High.Price.AssertEqual(0m);
889+
builder.Low.Price.AssertEqual(0m);
890+
}
891+
892+
[TestMethod]
893+
public void CandleHelper_PoC_EmptyCollection_ReturnsDefault()
894+
{
895+
// Test that PoC() on empty collection doesn't crash with InvalidOperationException
896+
var builder = new VolumeProfileBuilder();
897+
898+
// Should not throw InvalidOperationException from Max() on empty sequence
899+
var poc = builder.PoC();
900+
901+
// Should return default value
902+
poc.Price.AssertEqual(0m);
903+
poc.TotalVolume.AssertEqual(0m);
904+
}
905+
906+
[TestMethod]
907+
public void CandleHelper_PriceLevelOfMaxDelta_EmptyCollection_ReturnsDefault()
908+
{
909+
// Test that PriceLevelOfMaxDelta() on empty collection doesn't crash
910+
var builder = new VolumeProfileBuilder();
911+
912+
// Should not throw InvalidOperationException from Max() on empty sequence
913+
var level = builder.PriceLevelOfMaxDelta();
914+
915+
// Should return default value
916+
level.Price.AssertEqual(0m);
917+
level.TotalVolume.AssertEqual(0m);
918+
}
919+
920+
[TestMethod]
921+
public void CandleHelper_PriceLevelOfMinDelta_EmptyCollection_ReturnsDefault()
922+
{
923+
// Test that PriceLevelOfMinDelta() on empty collection doesn't crash
924+
var builder = new VolumeProfileBuilder();
925+
926+
// Should not throw InvalidOperationException from Min() on empty sequence
927+
var level = builder.PriceLevelOfMinDelta();
928+
929+
// Should return default value
930+
level.Price.AssertEqual(0m);
931+
level.TotalVolume.AssertEqual(0m);
932+
}
933+
934+
[TestMethod]
935+
public void CandleHelper_BuyVolAbovePoC_EmptyCollection_ReturnsZero()
936+
{
937+
// Test that BuyVolAbovePoC() handles empty collection correctly
938+
var builder = new VolumeProfileBuilder();
939+
940+
// Should return 0 instead of throwing or calculating with invalid PoC
941+
var vol = builder.BuyVolAbovePoC();
942+
943+
vol.AssertEqual(0m);
944+
}
945+
946+
[TestMethod]
947+
public void CandleHelper_BuyVolBelowPoC_EmptyCollection_ReturnsZero()
948+
{
949+
// Test that BuyVolBelowPoC() handles empty collection correctly
950+
var builder = new VolumeProfileBuilder();
951+
952+
// Should return 0 instead of throwing or calculating with invalid PoC
953+
var vol = builder.BuyVolBelowPoC();
954+
955+
vol.AssertEqual(0m);
956+
}
957+
958+
[TestMethod]
959+
public void CandleHelper_SellVolAbovePoC_EmptyCollection_ReturnsZero()
960+
{
961+
// Test that SellVolAbovePoC() handles empty collection correctly
962+
var builder = new VolumeProfileBuilder();
963+
964+
// Should return 0 instead of throwing or calculating with invalid PoC
965+
var vol = builder.SellVolAbovePoC();
966+
967+
vol.AssertEqual(0m);
968+
}
969+
970+
[TestMethod]
971+
public void CandleHelper_SellVolBelowPoC_EmptyCollection_ReturnsZero()
972+
{
973+
// Test that SellVolBelowPoC() handles empty collection correctly
974+
var builder = new VolumeProfileBuilder();
975+
976+
// Should return 0 instead of throwing or calculating with invalid PoC
977+
var vol = builder.SellVolBelowPoC();
978+
979+
vol.AssertEqual(0m);
980+
}
981+
982+
[TestMethod]
983+
public void CandleHelper_TotalBuyVolume_EmptyCollection_ReturnsZero()
984+
{
985+
// Test that TotalBuyVolume() handles empty collection correctly
986+
var builder = new VolumeProfileBuilder();
987+
988+
var vol = builder.TotalBuyVolume();
989+
990+
vol.AssertEqual(0m);
991+
}
992+
993+
[TestMethod]
994+
public void CandleHelper_TotalSellVolume_EmptyCollection_ReturnsZero()
995+
{
996+
// Test that TotalSellVolume() handles empty collection correctly
997+
var builder = new VolumeProfileBuilder();
998+
999+
var vol = builder.TotalSellVolume();
1000+
1001+
vol.AssertEqual(0m);
1002+
}
1003+
1004+
[TestMethod]
1005+
public void CandleHelper_Delta_EmptyCollection_ReturnsZero()
1006+
{
1007+
// Test that Delta() handles empty collection correctly
1008+
var builder = new VolumeProfileBuilder();
1009+
1010+
var delta = builder.Delta();
1011+
1012+
delta.AssertEqual(0m);
1013+
}
1014+
1015+
[TestMethod]
1016+
public void CandleHelper_PoC_WithData_ReturnsMaxVolumeLevel()
1017+
{
1018+
// Test that PoC() correctly returns the level with maximum volume
1019+
var builder = new VolumeProfileBuilder();
1020+
1021+
builder.Update(100m, 10m, Sides.Buy);
1022+
builder.Update(101m, 50m, Sides.Buy); // This should be PoC
1023+
builder.Update(102m, 20m, Sides.Sell);
1024+
1025+
var poc = builder.PoC();
1026+
1027+
poc.Price.AssertEqual(101m);
1028+
(poc.BuyVolume + poc.SellVolume).AssertEqual(50m);
1029+
}
1030+
1031+
[TestMethod]
1032+
public void CandleHelper_PriceLevelOfMaxDelta_WithData_ReturnsCorrectLevel()
1033+
{
1034+
// Test that PriceLevelOfMaxDelta() correctly finds maximum buy-sell delta
1035+
var builder = new VolumeProfileBuilder();
1036+
1037+
builder.Update(100m, 10m, Sides.Buy); // delta = +10
1038+
builder.Update(101m, 50m, Sides.Buy); // delta = +50 (MAX)
1039+
builder.Update(102m, 20m, Sides.Sell); // delta = -20
1040+
1041+
var level = builder.PriceLevelOfMaxDelta();
1042+
1043+
level.Price.AssertEqual(101m);
1044+
(level.BuyVolume - level.SellVolume).AssertEqual(50m);
1045+
}
1046+
1047+
[TestMethod]
1048+
public void CandleHelper_PriceLevelOfMinDelta_WithData_ReturnsCorrectLevel()
1049+
{
1050+
// Test that PriceLevelOfMinDelta() correctly finds minimum buy-sell delta
1051+
var builder = new VolumeProfileBuilder();
1052+
1053+
builder.Update(100m, 10m, Sides.Buy); // delta = +10
1054+
builder.Update(101m, 5m, Sides.Sell); // delta = -5
1055+
builder.Update(102m, 30m, Sides.Sell); // delta = -30 (MIN)
1056+
1057+
var level = builder.PriceLevelOfMinDelta();
1058+
1059+
level.Price.AssertEqual(102m);
1060+
(level.BuyVolume - level.SellVolume).AssertEqual(-30m);
1061+
}
1062+
1063+
[TestMethod]
1064+
public void CandleHelper_VolumesAboveBelowPoC_WithData_CalculatesCorrectly()
1065+
{
1066+
// Test that volume calculations above/below PoC work correctly
1067+
var builder = new VolumeProfileBuilder();
1068+
1069+
builder.Update(98m, 5m, Sides.Buy); // Below PoC
1070+
builder.Update(99m, 10m, Sides.Sell); // Below PoC
1071+
builder.Update(100m, 100m, Sides.Buy); // This is PoC (max volume)
1072+
builder.Update(101m, 15m, Sides.Buy); // Above PoC
1073+
builder.Update(102m, 20m, Sides.Sell); // Above PoC
1074+
1075+
// Verify PoC is correct
1076+
var poc = builder.PoC();
1077+
poc.Price.AssertEqual(100m);
1078+
1079+
// Test volumes above PoC
1080+
builder.BuyVolAbovePoC().AssertEqual(15m);
1081+
builder.SellVolAbovePoC().AssertEqual(20m);
1082+
builder.VolumeAbovePoC().AssertEqual(35m);
1083+
1084+
// Test volumes below PoC
1085+
builder.BuyVolBelowPoC().AssertEqual(5m);
1086+
builder.SellVolBelowPoC().AssertEqual(10m);
1087+
builder.VolumeBelowPoC().AssertEqual(15m);
1088+
1089+
// Test delta calculations
1090+
builder.DeltaAbovePoC().AssertEqual(-5m); // 15 - 20
1091+
builder.DeltaBelowPoC().AssertEqual(-5m); // 5 - 10
1092+
}
8751093
}

0 commit comments

Comments
 (0)