Skip to content

Commit 5ea6186

Browse files
committed
Add test to demonstrate stale price bug in PnL calculation
Added StalePrices_QuoteThenCandle test that reveals a bug where ProcessCandle/ProcessQuotes/ProcessExecution methods don't reset other price fields, leading to stale prices being used for UnrealizedPnL calculation. The test sequence: 1. Quote sets bidPrice=130 2. Candle sets lastPrice=150 but doesn't clear bidPrice 3. UnrealizedPnL uses stale bidPrice=130 instead of fresh lastPrice=150 This explains why existing tests pass - they always have ProcessLevel1 between different data types, which correctly resets all prices. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f3248cb commit 5ea6186

1 file changed

Lines changed: 65 additions & 2 deletions

File tree

Tests/PnLTests.cs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,76 @@ 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+
// 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+
// Quote: 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+
// Candle closes at 150 (newer data!)
468+
var candle = new TimeFrameCandleMessage
469+
{
470+
SecurityId = secId,
471+
OpenTime = DateTimeOffset.Now,
472+
CloseTime = DateTimeOffset.Now,
473+
OpenPrice = 100,
474+
HighPrice = 150,
475+
LowPrice = 90,
476+
ClosePrice = 150,
477+
TotalVolume = 1
478+
};
479+
manager.ProcessMessage(candle);
480+
481+
// BUG: UnrealizedPnL still uses stale bid=130 instead of fresh close=150
482+
// Expected: (150-100)*1 = 50
483+
// Actual: (130-100)*1 = 30 ❌
484+
manager.UnrealizedPnL.AssertEqual(50m);
485+
}
423486
}

0 commit comments

Comments
 (0)