Skip to content

Commit 22a6e90

Browse files
authored
Merge pull request StockSharp#666 from StockSharp/claude/debug-algo-pnl-011CUM4Kzn1mECA7KNRCXUEp
Debug Algo/PnL Calculation Errors
2 parents 38ba53a + 8d2233e commit 22a6e90

1 file changed

Lines changed: 250 additions & 2 deletions

File tree

Tests/PnLTests.cs

Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,261 @@ public void UnrealizedPnL_ByDataType()
411411
public void SaveLoad()
412412
{
413413
var manager = new PnLManager { UseLevel1 = true };
414-
414+
415415
var storage = manager.Save();
416416

417417
var manager2 = new PnLManager();
418-
418+
419419
manager2.UseLevel1.AssertFalse();
420420
manager2.Load(storage);
421421
manager2.UseLevel1.AssertTrue();
422422
}
423+
424+
[TestMethod]
425+
public void StalePrices_QuoteThenCandle()
426+
{
427+
var secId = Helper.CreateSecurityId();
428+
429+
IPnLManager manager = new PnLManager
430+
{
431+
UseOrderBook = true,
432+
UseCandles = true
433+
};
434+
435+
var reg = new OrderRegisterMessage
436+
{
437+
PortfolioName = Helper.CreatePortfolio().Name,
438+
SecurityId = secId,
439+
TransactionId = 1,
440+
};
441+
manager.ProcessMessage(reg);
442+
443+
// Open long position: buy 1 at 100
444+
var buy = new ExecutionMessage
445+
{
446+
OriginalTransactionId = reg.TransactionId,
447+
DataTypeEx = DataType.Transactions,
448+
SecurityId = secId,
449+
TradeId = 1,
450+
TradePrice = 100m,
451+
TradeVolume = 1m,
452+
Side = Sides.Buy,
453+
ServerTime = DateTimeOffset.UtcNow
454+
};
455+
manager.ProcessMessage(buy);
456+
457+
// Market data: quote with bid=130, ask=131
458+
var quote = new QuoteChangeMessage
459+
{
460+
SecurityId = secId,
461+
Bids = [new(130m, 1)],
462+
Asks = [new(131m, 1)]
463+
};
464+
manager.ProcessMessage(quote);
465+
manager.UnrealizedPnL.AssertEqual(30m); // (130-100)*1 = 30
466+
467+
// Newer market data: candle closes at 150
468+
// BUG: ProcessCandle doesn't clear bid/ask prices, so UnrealizedPnL
469+
// continues to use stale bid=130 instead of fresh close=150
470+
var candle = new TimeFrameCandleMessage
471+
{
472+
SecurityId = secId,
473+
OpenTime = DateTimeOffset.Now,
474+
CloseTime = DateTimeOffset.Now,
475+
OpenPrice = 100,
476+
HighPrice = 150,
477+
LowPrice = 90,
478+
ClosePrice = 150,
479+
TotalVolume = 1
480+
};
481+
manager.ProcessMessage(candle);
482+
483+
// Expected: (150-100)*1 = 50
484+
// Actual: (130-100)*1 = 30 (uses stale bid instead of fresh close price)
485+
manager.UnrealizedPnL.AssertEqual(50m);
486+
}
487+
488+
[TestMethod]
489+
public void StalePrices_CandleThenQuote()
490+
{
491+
var secId = Helper.CreateSecurityId();
492+
493+
IPnLManager manager = new PnLManager
494+
{
495+
UseOrderBook = true,
496+
UseCandles = true
497+
};
498+
499+
var reg = new OrderRegisterMessage
500+
{
501+
PortfolioName = Helper.CreatePortfolio().Name,
502+
SecurityId = secId,
503+
TransactionId = 1,
504+
};
505+
manager.ProcessMessage(reg);
506+
507+
// Open short position: sell 1 at 100
508+
var sell = new ExecutionMessage
509+
{
510+
OriginalTransactionId = reg.TransactionId,
511+
DataTypeEx = DataType.Transactions,
512+
SecurityId = secId,
513+
TradeId = 1,
514+
TradePrice = 100m,
515+
TradeVolume = 1m,
516+
Side = Sides.Sell,
517+
ServerTime = DateTimeOffset.UtcNow
518+
};
519+
manager.ProcessMessage(sell);
520+
521+
// Market data: candle closes at 90
522+
var candle = new TimeFrameCandleMessage
523+
{
524+
SecurityId = secId,
525+
OpenTime = DateTimeOffset.Now,
526+
CloseTime = DateTimeOffset.Now,
527+
OpenPrice = 100,
528+
HighPrice = 110,
529+
LowPrice = 85,
530+
ClosePrice = 90,
531+
TotalVolume = 1
532+
};
533+
manager.ProcessMessage(candle);
534+
manager.UnrealizedPnL.AssertEqual(10m); // (100-90)*1 = 10
535+
536+
// Newer market data: quote with ask=80
537+
// BUG: ProcessQuotes doesn't clear lastPrice, so if ask is not available,
538+
// UnrealizedPnL uses stale close=90 instead of fresh ask=80
539+
var quote = new QuoteChangeMessage
540+
{
541+
SecurityId = secId,
542+
Bids = [new(79m, 1)],
543+
Asks = [new(80m, 1)]
544+
};
545+
manager.ProcessMessage(quote);
546+
547+
// For short position, uses ask price for UnrealizedPnL
548+
// Expected: (100-80)*1 = 20
549+
manager.UnrealizedPnL.AssertEqual(20m);
550+
}
551+
552+
[TestMethod]
553+
public void StalePrices_TickThenQuote()
554+
{
555+
var secId = Helper.CreateSecurityId();
556+
557+
IPnLManager manager = new PnLManager
558+
{
559+
UseTick = true,
560+
UseOrderBook = true,
561+
};
562+
563+
var reg = new OrderRegisterMessage
564+
{
565+
PortfolioName = Helper.CreatePortfolio().Name,
566+
SecurityId = secId,
567+
TransactionId = 1,
568+
};
569+
manager.ProcessMessage(reg);
570+
571+
// Open long position: buy 1 at 100
572+
var buy = new ExecutionMessage
573+
{
574+
OriginalTransactionId = reg.TransactionId,
575+
DataTypeEx = DataType.Transactions,
576+
SecurityId = secId,
577+
TradeId = 1,
578+
TradePrice = 100m,
579+
TradeVolume = 1m,
580+
Side = Sides.Buy,
581+
ServerTime = DateTimeOffset.UtcNow
582+
};
583+
manager.ProcessMessage(buy);
584+
585+
// Market data: tick at 140
586+
var tick = new ExecutionMessage
587+
{
588+
DataTypeEx = DataType.Ticks,
589+
SecurityId = secId,
590+
TradePrice = 140m,
591+
ServerTime = DateTimeOffset.UtcNow
592+
};
593+
manager.ProcessMessage(tick);
594+
manager.UnrealizedPnL.AssertEqual(40m); // (140-100)*1 = 40
595+
596+
// Newer market data: quote with bid=130
597+
// BUG: ProcessQuotes doesn't clear lastPrice, so UnrealizedPnL
598+
// prefers bid=130 over stale lastPrice=140
599+
var quote = new QuoteChangeMessage
600+
{
601+
SecurityId = secId,
602+
Bids = [new(130m, 1)],
603+
Asks = [new(131m, 1)]
604+
};
605+
manager.ProcessMessage(quote);
606+
607+
// For long position, uses bid price (which is more accurate than last trade)
608+
// Expected: (130-100)*1 = 30
609+
manager.UnrealizedPnL.AssertEqual(30m);
610+
}
611+
612+
[TestMethod]
613+
public void StalePrices_QuoteThenTick()
614+
{
615+
var secId = Helper.CreateSecurityId();
616+
617+
IPnLManager manager = new PnLManager
618+
{
619+
UseTick = true,
620+
UseOrderBook = true,
621+
};
622+
623+
var reg = new OrderRegisterMessage
624+
{
625+
PortfolioName = Helper.CreatePortfolio().Name,
626+
SecurityId = secId,
627+
TransactionId = 1,
628+
};
629+
manager.ProcessMessage(reg);
630+
631+
// Open long position: buy 1 at 100
632+
var buy = new ExecutionMessage
633+
{
634+
OriginalTransactionId = reg.TransactionId,
635+
DataTypeEx = DataType.Transactions,
636+
SecurityId = secId,
637+
TradeId = 1,
638+
TradePrice = 100m,
639+
TradeVolume = 1m,
640+
Side = Sides.Buy,
641+
ServerTime = DateTimeOffset.UtcNow
642+
};
643+
manager.ProcessMessage(buy);
644+
645+
// Market data: quote with bid=130
646+
var quote = new QuoteChangeMessage
647+
{
648+
SecurityId = secId,
649+
Bids = [new(130m, 1)],
650+
Asks = [new(131m, 1)]
651+
};
652+
manager.ProcessMessage(quote);
653+
manager.UnrealizedPnL.AssertEqual(30m); // (130-100)*1 = 30
654+
655+
// Newer market data: tick at 150
656+
// BUG: ProcessExecution doesn't clear bid/ask prices, so UnrealizedPnL
657+
// continues to use stale bid=130 instead of fresh tick=150
658+
var tick = new ExecutionMessage
659+
{
660+
DataTypeEx = DataType.Ticks,
661+
SecurityId = secId,
662+
TradePrice = 150m,
663+
ServerTime = DateTimeOffset.UtcNow
664+
};
665+
manager.ProcessMessage(tick);
666+
667+
// Expected: (150-100)*1 = 50
668+
// Actual: (130-100)*1 = 30 (uses stale bid instead of fresh tick)
669+
manager.UnrealizedPnL.AssertEqual(50m);
670+
}
423671
}

0 commit comments

Comments
 (0)