Skip to content

Commit 2c0468c

Browse files
committed
commit
1 parent c9e70ba commit 2c0468c

2 files changed

Lines changed: 634 additions & 0 deletions

File tree

internal/ibctl/ibctlmerge/ibctlmerge.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
410476
func csvDividendsToIncome(dividends []ibkractivitycsv.Dividend, accountAlias string) []*datav1.Income {
411477
var result []*datav1.Income

0 commit comments

Comments
 (0)