@@ -440,7 +440,7 @@ public void StalePrices_QuoteThenCandle()
440440 } ;
441441 manager . ProcessMessage ( reg ) ;
442442
443- // Buy 1 at 100
443+ // Open long position: buy 1 at 100
444444 var buy = new ExecutionMessage
445445 {
446446 OriginalTransactionId = reg . TransactionId ,
@@ -454,7 +454,7 @@ public void StalePrices_QuoteThenCandle()
454454 } ;
455455 manager . ProcessMessage ( buy ) ;
456456
457- // Quote: bid=130, ask=131
457+ // Market data: quote with bid=130, ask=131
458458 var quote = new QuoteChangeMessage
459459 {
460460 SecurityId = secId ,
@@ -464,7 +464,9 @@ public void StalePrices_QuoteThenCandle()
464464 manager . ProcessMessage ( quote ) ;
465465 manager . UnrealizedPnL . AssertEqual ( 30m ) ; // (130-100)*1 = 30
466466
467- // Candle closes at 150 (newer data!)
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
468470 var candle = new TimeFrameCandleMessage
469471 {
470472 SecurityId = secId ,
@@ -478,9 +480,192 @@ public void StalePrices_QuoteThenCandle()
478480 } ;
479481 manager . ProcessMessage ( candle ) ;
480482
481- // BUG: UnrealizedPnL still uses stale bid=130 instead of fresh close=150
482483 // Expected: (150-100)*1 = 50
483- // Actual: (130-100)*1 = 30 ❌
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)
484669 manager . UnrealizedPnL . AssertEqual ( 50m ) ;
485670 }
486671}
0 commit comments