@@ -180,6 +180,10 @@ func Merge(
180180 // Convert config additions to synthetic Trade protos.
181181 // These are loaded from config every time, not persisted to trades.json.
182182 allTrades = append (allTrades , additionsToTrades (additions )... )
183+ // Adjust reported positions to account for additions so that VerifyPositions
184+ // doesn't flag expected mismatches. Updates quantity and cost basis for
185+ // existing positions, and creates synthetic positions for new symbols.
186+ allPositions = applyAdditionsToPositions (allPositions , additions )
183187 // Sort all trades by date for deterministic output.
184188 sort .Slice (allTrades , func (i , j int ) bool {
185189 dateI := protoDateString (allTrades [i ].GetTradeDate ())
@@ -406,6 +410,68 @@ func additionsToTrades(additions []ibctlconfig.Addition) []*datav1.Trade {
406410 return trades
407411}
408412
413+ // applyAdditionsToPositions adjusts reported positions to include addition trades.
414+ // For existing (account, symbol) pairs it updates quantity and recomputes the weighted
415+ // average cost basis. For new symbols it creates a synthetic position so that
416+ // VerifyPositions doesn't flag expected mismatches from additions.
417+ // Returns the (potentially extended) positions slice.
418+ func applyAdditionsToPositions (positions []* datav1.Position , additions []ibctlconfig.Addition ) []* datav1.Position {
419+ if len (additions ) == 0 {
420+ return positions
421+ }
422+ type posKey struct {
423+ accountAlias string
424+ symbol string
425+ }
426+ // Build a lookup from (account_alias, symbol) to the position slice index.
427+ posIndex := make (map [posKey ]int , len (positions ))
428+ for i , pos := range positions {
429+ posIndex [posKey {accountAlias : pos .GetAccountAlias (), symbol : pos .GetSymbol ()}] = i
430+ }
431+ for _ , addition := range additions {
432+ key := posKey {accountAlias : addition .AccountAlias , symbol : addition .Symbol }
433+ // Buys add to quantity, sells subtract.
434+ additionQtyMicros := addition .Quantity
435+ if addition .TradeSide == datav1 .TradeSide_TRADE_SIDE_SELL {
436+ additionQtyMicros = - additionQtyMicros
437+ }
438+ idx , ok := posIndex [key ]
439+ if ok {
440+ // Existing position: adjust quantity and recompute weighted average cost basis.
441+ pos := positions [idx ]
442+ oldQtyMicros := mathpb .ToMicros (pos .GetQuantity ())
443+ oldCostMicros := moneypb .MoneyToMicros (pos .GetCostBasisPrice ())
444+ newQtyMicros := oldQtyMicros + additionQtyMicros
445+ // Weighted average: (oldQty * oldCost + addQty * addCost) / newQty.
446+ // Both cost and quantity are in micros, so divide by 1_000_000 to stay in micros.
447+ if newQtyMicros != 0 {
448+ newCostMicros := (oldQtyMicros * oldCostMicros + additionQtyMicros * addition .TradePrice ) / newQtyMicros
449+ pos .CostBasisPrice = moneypb .MoneyFromMicros (pos .GetCurrencyCode (), newCostMicros )
450+ }
451+ pos .Quantity = mathpb .FromMicros (newQtyMicros )
452+ } else {
453+ // New (account, symbol) pair: create a synthetic position so VerifyPositions
454+ // doesn't flag it as DiscrepancyTypeComputedOnly.
455+ newPos := & datav1.Position {
456+ Symbol : addition .Symbol ,
457+ AssetCategory : "STK" ,
458+ Quantity : mathpb .FromMicros (additionQtyMicros ),
459+ CostBasisPrice : moneypb .MoneyFromMicros (addition .CurrencyCode , addition .TradePrice ),
460+ // Use trade price as the market price placeholder; realtime overrides
461+ // will replace this if --realtime is used.
462+ MarketPrice : moneypb .MoneyFromMicros (addition .CurrencyCode , addition .TradePrice ),
463+ MarketValue : moneypb .MoneyFromMicros (addition .CurrencyCode , 0 ),
464+ CurrencyCode : addition .CurrencyCode ,
465+ AccountAlias : addition .AccountAlias ,
466+ }
467+ positions = append (positions , newPos )
468+ // Track the new position for subsequent additions of the same (account, symbol).
469+ posIndex [key ] = len (positions ) - 1
470+ }
471+ }
472+ return positions
473+ }
474+
409475// csvDividendsToIncome converts Activity Statement CSV dividends to Income protos.
410476func csvDividendsToIncome (dividends []ibkractivitycsv.Dividend , accountAlias string ) []* datav1.Income {
411477 var result []* datav1.Income
0 commit comments