Skip to content

Commit a351913

Browse files
committed
commit
1 parent bceefc4 commit a351913

File tree

10 files changed

+646
-0
lines changed

10 files changed

+646
-0
lines changed

cmd/ibctl/internal/command/holding/category/categorylist/categorylist.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"os"
1111
"strings"
12+
"time"
1213

1314
"buf.build/go/app/appcmd"
1415
"buf.build/go/app/appext"
@@ -18,6 +19,7 @@ import (
1819
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
1920
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
2021
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
22+
"github.com/bufdev/ibctl/internal/ibctl/ibctlrealtime"
2123
"github.com/bufdev/ibctl/internal/pkg/cliio"
2224
"github.com/spf13/pflag"
2325
)
@@ -31,6 +33,8 @@ const (
3133
geoFlagName = "geo"
3234
// baseCurrencyFlagName is the flag name for the base currency for value conversion.
3335
baseCurrencyFlagName = "base-currency"
36+
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
37+
realtimeFlagName = "realtime"
3438
)
3539

3640
// NewCommand returns a new category list command.
@@ -67,6 +71,8 @@ type flags struct {
6771
Geo string
6872
// BaseCurrency is the target currency for value conversion (e.g., "USD", "CAD").
6973
BaseCurrency string
74+
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
75+
Realtime bool
7076
}
7177

7278
func newFlags() *flags {
@@ -79,6 +85,7 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
7985
flagSet.BoolVar(&f.Download, downloadFlagName, false, "Download fresh data before displaying")
8086
flagSet.StringVar(&f.Geo, geoFlagName, "", "Filter by geo (e.g., US, INTL)")
8187
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
88+
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
8289
}
8390

8491
func run(ctx context.Context, container appext.Container, flags *flags) error {
@@ -124,6 +131,13 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
124131
}
125132
// Load FX rates for base currency conversion.
126133
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.DirPath))
134+
// Override market prices and FX rates with real-time data from Yahoo Finance.
135+
if flags.Realtime {
136+
todayDate := time.Now().Format("2006-01-02")
137+
if err := ibctlrealtime.ApplyOverrides(ctx, mergedData.Positions, fxStore, config, baseCurrency, todayDate); err != nil {
138+
return err
139+
}
140+
}
127141
// Compute holdings via FIFO from all trade data, converted to the base currency.
128142
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, mergedData.CashPositions, config, fxStore, baseCurrency)
129143
if err != nil {

cmd/ibctl/internal/command/holding/geo/geolist/geolist.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"os"
1111
"strings"
12+
"time"
1213

1314
"buf.build/go/app/appcmd"
1415
"buf.build/go/app/appext"
@@ -18,6 +19,7 @@ import (
1819
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
1920
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
2021
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
22+
"github.com/bufdev/ibctl/internal/ibctl/ibctlrealtime"
2123
"github.com/bufdev/ibctl/internal/pkg/cliio"
2224
"github.com/spf13/pflag"
2325
)
@@ -31,6 +33,8 @@ const (
3133
categoryFlagName = "category"
3234
// baseCurrencyFlagName is the flag name for the base currency for value conversion.
3335
baseCurrencyFlagName = "base-currency"
36+
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
37+
realtimeFlagName = "realtime"
3438
)
3539

3640
// NewCommand returns a new geo list command.
@@ -68,6 +72,8 @@ type flags struct {
6872
Category string
6973
// BaseCurrency is the target currency for value conversion (e.g., "USD", "CAD").
7074
BaseCurrency string
75+
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
76+
Realtime bool
7177
}
7278

7379
func newFlags() *flags {
@@ -80,6 +86,7 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
8086
flagSet.BoolVar(&f.Download, downloadFlagName, false, "Download fresh data before displaying")
8187
flagSet.StringVar(&f.Category, categoryFlagName, "", "Filter by category (e.g., EQUITY, FIXED_INCOME)")
8288
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
89+
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
8390
}
8491

8592
func run(ctx context.Context, container appext.Container, flags *flags) error {
@@ -125,6 +132,13 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
125132
}
126133
// Load FX rates for base currency conversion.
127134
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.DirPath))
135+
// Override market prices and FX rates with real-time data from Yahoo Finance.
136+
if flags.Realtime {
137+
todayDate := time.Now().Format("2006-01-02")
138+
if err := ibctlrealtime.ApplyOverrides(ctx, mergedData.Positions, fxStore, config, baseCurrency, todayDate); err != nil {
139+
return err
140+
}
141+
}
128142
// Compute holdings via FIFO from all trade data, converted to the base currency.
129143
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, mergedData.CashPositions, config, fxStore, baseCurrency)
130144
if err != nil {

cmd/ibctl/internal/command/holding/holdinglist/holdinglist.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"os"
1111
"strings"
12+
"time"
1213

1314
"buf.build/go/app/appcmd"
1415
"buf.build/go/app/appext"
@@ -18,6 +19,7 @@ import (
1819
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
1920
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
2021
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
22+
"github.com/bufdev/ibctl/internal/ibctl/ibctlrealtime"
2123
"github.com/bufdev/ibctl/internal/ibctl/ibctltaxlot"
2224
"github.com/bufdev/ibctl/internal/pkg/cliio"
2325
"github.com/bufdev/ibctl/internal/pkg/mathpb"
@@ -32,6 +34,8 @@ const (
3234
downloadFlagName = "download"
3335
// baseCurrencyFlagName is the flag name for the base currency for value conversion.
3436
baseCurrencyFlagName = "base-currency"
37+
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
38+
realtimeFlagName = "realtime"
3539
)
3640

3741
// NewCommand returns a new holdings overview command.
@@ -96,6 +100,8 @@ type flags struct {
96100
Download bool
97101
// BaseCurrency is the target currency for value conversion (e.g., "USD", "CAD").
98102
BaseCurrency string
103+
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
104+
Realtime bool
99105
}
100106

101107
func newFlags() *flags {
@@ -107,6 +113,7 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
107113
flagSet.StringVar(&f.Format, formatFlagName, "table", "Output format (table, csv, json)")
108114
flagSet.BoolVar(&f.Download, downloadFlagName, false, "Download fresh data before displaying")
109115
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
116+
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
110117
}
111118

112119
func run(ctx context.Context, container appext.Container, flags *flags) error {
@@ -154,6 +161,13 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
154161
}
155162
// Load FX rates for base currency conversion.
156163
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.DirPath))
164+
// Override market prices and FX rates with real-time data from Yahoo Finance.
165+
if flags.Realtime {
166+
todayDate := time.Now().Format("2006-01-02")
167+
if err := ibctlrealtime.ApplyOverrides(ctx, mergedData.Positions, fxStore, config, baseCurrency, todayDate); err != nil {
168+
return err
169+
}
170+
}
157171
// Compute holdings via FIFO from all trade data, verified against IBKR positions.
158172
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, mergedData.CashPositions, config, fxStore, baseCurrency)
159173
if err != nil {

cmd/ibctl/internal/command/holding/holdingvalue/holdingvalue.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"math"
1212
"os"
1313
"strings"
14+
"time"
1415

1516
"buf.build/go/app/appcmd"
1617
"buf.build/go/app/appext"
@@ -20,6 +21,7 @@ import (
2021
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
2122
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
2223
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
24+
"github.com/bufdev/ibctl/internal/ibctl/ibctlrealtime"
2325
"github.com/bufdev/ibctl/internal/pkg/cliio"
2426
"github.com/bufdev/ibctl/internal/pkg/mathpb"
2527
"github.com/bufdev/ibctl/internal/pkg/moneypb"
@@ -31,6 +33,8 @@ const (
3133
downloadFlagName = "download"
3234
// baseCurrencyFlagName is the flag name for the base currency for value conversion.
3335
baseCurrencyFlagName = "base-currency"
36+
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
37+
realtimeFlagName = "realtime"
3438
)
3539

3640
// NewCommand returns a new holding value command.
@@ -70,6 +74,8 @@ type flags struct {
7074
Download bool
7175
// BaseCurrency is the target currency for value conversion (e.g., "USD", "CAD").
7276
BaseCurrency string
77+
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
78+
Realtime bool
7379
}
7480

7581
func newFlags() *flags {
@@ -80,6 +86,7 @@ func newFlags() *flags {
8086
func (f *flags) Bind(flagSet *pflag.FlagSet) {
8187
flagSet.BoolVar(&f.Download, downloadFlagName, false, "Download fresh data before displaying")
8288
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
89+
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
8390
}
8491

8592
func run(ctx context.Context, container appext.Container, flags *flags) error {
@@ -121,6 +128,13 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
121128
}
122129
// Load FX rates for base currency conversion.
123130
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.DirPath))
131+
// Override market prices and FX rates with real-time data from Yahoo Finance.
132+
if flags.Realtime {
133+
todayDate := time.Now().Format("2006-01-02")
134+
if err := ibctlrealtime.ApplyOverrides(ctx, mergedData.Positions, fxStore, config, baseCurrency, todayDate); err != nil {
135+
return err
136+
}
137+
}
124138
// Compute holdings via FIFO from all trade data, converted to the base currency.
125139
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, mergedData.CashPositions, config, fxStore, baseCurrency)
126140
if err != nil {

cmd/ibctl/internal/command/holding/lot/lotlist/lotlist.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"os"
1111
"strings"
12+
"time"
1213

1314
"buf.build/go/app/appcmd"
1415
"buf.build/go/app/appext"
@@ -18,6 +19,7 @@ import (
1819
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
1920
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
2021
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
22+
"github.com/bufdev/ibctl/internal/ibctl/ibctlrealtime"
2123
"github.com/bufdev/ibctl/internal/pkg/cliio"
2224
"github.com/bufdev/ibctl/internal/pkg/moneypb"
2325
"github.com/spf13/pflag"
@@ -32,6 +34,8 @@ const (
3234
symbolFlagName = "symbol"
3335
// baseCurrencyFlagName is the flag name for the base currency for value conversion.
3436
baseCurrencyFlagName = "base-currency"
37+
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
38+
realtimeFlagName = "realtime"
3539
)
3640

3741
// NewCommand returns a new lot list command.
@@ -76,6 +80,8 @@ type flags struct {
7680
Symbol string
7781
// BaseCurrency is the target currency for value conversion (e.g., "USD", "CAD").
7882
BaseCurrency string
83+
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
84+
Realtime bool
7985
}
8086

8187
func newFlags() *flags {
@@ -88,6 +94,7 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
8894
flagSet.BoolVar(&f.Download, downloadFlagName, false, "Download fresh data before displaying")
8995
flagSet.StringVar(&f.Symbol, symbolFlagName, "", "Filter by symbol (omit for all symbols)")
9096
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
97+
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
9198
}
9299

93100
func run(ctx context.Context, container appext.Container, flags *flags) error {
@@ -134,6 +141,13 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
134141
}
135142
// Load FX rates for base currency conversion.
136143
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.DirPath))
144+
// Override market prices and FX rates with real-time data from Yahoo Finance.
145+
if flags.Realtime {
146+
todayDate := time.Now().Format("2006-01-02")
147+
if err := ibctlrealtime.ApplyOverrides(ctx, mergedData.Positions, fxStore, config, baseCurrency, todayDate); err != nil {
148+
return err
149+
}
150+
}
137151
// Get the lot list, optionally filtered by symbol, converted to the base currency.
138152
result, err := ibctlholdings.GetLotList(flags.Symbol, mergedData.Trades, mergedData.Positions, config, fxStore, baseCurrency)
139153
if err != nil {

internal/ibctl/ibctlconfig/ibctlconfig.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ accounts:
5656
# type: STOCK
5757
# sector: TECH
5858
# geo: US
59+
# Real-time symbol overrides for --realtime flag.
60+
#
61+
# Optional. Maps IBKR symbols to Yahoo Finance symbols for international tickers
62+
# where symbols differ. US-listed securities typically need no mapping.
63+
# realtime_symbols:
64+
# SHOP: SHOP.TO
65+
# RY: RY.TO
5966
`
6067

6168
// ExternalConfigV1 is the YAML-serializable configuration file structure for version v1.
@@ -75,6 +82,9 @@ type ExternalConfigV1 struct {
7582
Taxes *ExternalTaxConfigV1 `yaml:"taxes"`
7683
// Additions is the list of manually added trades from non-IBKR brokers.
7784
Additions []ExternalAdditionV1 `yaml:"additions"`
85+
// RealtimeSymbols maps IBKR symbols to Yahoo Finance symbols for --realtime quote lookups.
86+
// Only needed for international tickers where symbols differ (e.g., SHOP → SHOP.TO).
87+
RealtimeSymbols map[string]string `yaml:"realtime_symbols"`
7888
}
7989

8090
// ExternalTaxConfigV1 holds capital gains tax rate configuration.
@@ -147,6 +157,9 @@ type Config struct {
147157
// AdditionLastPrices maps symbols to their fallback last price in micros.
148158
// Used when no IBKR position exists for the symbol.
149159
AdditionLastPrices map[string]int64
160+
// RealtimeSymbols maps IBKR symbols to Yahoo Finance symbols for --realtime quote lookups.
161+
// Only needed for international tickers where symbols differ (e.g., SHOP → SHOP.TO).
162+
RealtimeSymbols map[string]string
150163
}
151164

152165
// SymbolConfig holds classification metadata for a symbol.
@@ -248,6 +261,11 @@ func NewConfigV1(externalConfig ExternalConfigV1, dirPath string) (*Config, erro
248261
if err != nil {
249262
return nil, err
250263
}
264+
// Build realtime symbols map, defaulting to empty if not configured.
265+
realtimeSymbols := externalConfig.RealtimeSymbols
266+
if realtimeSymbols == nil {
267+
realtimeSymbols = make(map[string]string)
268+
}
251269
return &Config{
252270
DirPath: dirPath,
253271
IBKRFlexQueryID: externalConfig.FlexQueryID,
@@ -259,6 +277,7 @@ func NewConfigV1(externalConfig ExternalConfigV1, dirPath string) (*Config, erro
259277
TaxRateLTCG: taxRateLTCG,
260278
Additions: additions,
261279
AdditionLastPrices: additionLastPrices,
280+
RealtimeSymbols: realtimeSymbols,
262281
}, nil
263282
}
264283

internal/ibctl/ibctlfxrates/ibctlfxrates.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,31 @@ func (s *Store) ConvertOnDate(valueMicros int64, baseCurrency string, quoteCurre
126126
return multiplyMicros(valueMicros, rateMicros), true
127127
}
128128

129+
// OverrideLatestRates injects real-time FX rates into the in-memory cache.
130+
// Each key in pairRatesMicros is a pair key (e.g., "USD.CAD") and the value
131+
// is the rate in micros. This is used by --realtime to apply fresh rates
132+
// for the current command run without persisting to disk.
133+
func (s *Store) OverrideLatestRates(todayDate string, pairRatesMicros map[string]int64) {
134+
s.mu.Lock()
135+
defer s.mu.Unlock()
136+
for pairKey, rateMicros := range pairRatesMicros {
137+
pair, loaded := s.pairs[pairKey]
138+
if !loaded || pair == nil {
139+
// Create a new pair entry for a pair not yet loaded from disk.
140+
pair = &pairData{
141+
rates: make(map[string]int64),
142+
}
143+
s.pairs[pairKey] = pair
144+
}
145+
// Override the latest rate and add today's date entry so that
146+
// both ConvertToUSD (uses latestRateMicros) and RateOnOrBefore/ConvertOnDate
147+
// (uses rates map) pick up the real-time value.
148+
pair.latestRateMicros = rateMicros
149+
pair.latestDate = todayDate
150+
pair.rates[todayDate] = rateMicros
151+
}
152+
}
153+
129154
// *** PRIVATE ***
130155

131156
// pairData holds the loaded rate data for a single currency pair.

0 commit comments

Comments
 (0)