Skip to content

Commit a8f2302

Browse files
committed
commit
1 parent 21b49f0 commit a8f2302

11 files changed

Lines changed: 490 additions & 189 deletions

File tree

cmd/ibctl/internal/command/holdings/holdingsoverview/holdingsoverview.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/bufdev/ibctl/internal/ibctl/ibctlfxrates"
1717
"github.com/bufdev/ibctl/internal/ibctl/ibctlholdings"
1818
"github.com/bufdev/ibctl/internal/ibctl/ibctlmerge"
19+
"github.com/bufdev/ibctl/internal/ibctl/ibctlpath"
1920
"github.com/bufdev/ibctl/internal/ibctl/ibctltaxlot"
2021
"github.com/bufdev/ibctl/internal/pkg/cliio"
2122
"github.com/bufdev/ibctl/internal/pkg/mathpb"
@@ -85,15 +86,21 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
8586
}
8687
}
8788
// Merge seed lots + Activity Statement CSVs + Flex Query cached data across all accounts.
88-
// Use AccountsDirPath for per-account cached data.
89-
mergedData, err := ibctlmerge.Merge(config.AccountsDirPath, config.ActivityStatementsDirPath, config.SeedDirPath, config.AccountAliases)
89+
// Trades come from data_dir (persistent), snapshots from cache_dir (blow-away safe).
90+
mergedData, err := ibctlmerge.Merge(
91+
ibctlpath.DataAccountsDirPath(config.DataDirPath),
92+
ibctlpath.CacheAccountsDirPath(config.CacheDirPath),
93+
config.ActivityStatementsDirPath,
94+
config.SeedDirPath,
95+
config.AccountAliases,
96+
)
9097
if err != nil {
9198
return err
9299
}
93100
// Load FX rates for USD price conversion. Returns an empty store if no data available.
94-
fxStore := ibctlfxrates.NewStore(config.FXDirPath)
101+
fxStore := ibctlfxrates.NewStore(ibctlpath.CacheFXDirPath(config.CacheDirPath))
95102
// Compute holdings via FIFO from all trade data, verified against IBKR positions.
96-
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, config, fxStore)
103+
result, err := ibctlholdings.GetHoldingsOverview(mergedData.Trades, mergedData.Positions, mergedData.CashPositions, config, fxStore)
97104
if err != nil {
98105
return err
99106
}
@@ -114,9 +121,23 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
114121
switch format {
115122
case cliio.FormatTable:
116123
headers := ibctlholdings.HoldingsOverviewHeaders()
117-
rows := make([][]string, 0, len(result.Holdings))
124+
// Split holdings into securities and cash for separate display sections.
125+
var securityRows, cashRows [][]string
118126
for _, h := range result.Holdings {
119-
rows = append(rows, ibctlholdings.HoldingOverviewToTableRow(h))
127+
row := ibctlholdings.HoldingOverviewToTableRow(h)
128+
if h.Category == "CASH" {
129+
cashRows = append(cashRows, row)
130+
} else {
131+
securityRows = append(securityRows, row)
132+
}
133+
}
134+
// Build the row sections: securities, then cash, then totals.
135+
var sections [][]string
136+
sections = append(sections, securityRows...)
137+
if len(cashRows) > 0 {
138+
// Blank separator row between securities and cash.
139+
sections = append(sections, make([]string, len(headers)))
140+
sections = append(sections, cashRows...)
120141
}
121142
// Build the totals row aligned to the same columns as the data.
122143
totals := ibctlholdings.ComputeTotals(result.Holdings)
@@ -126,7 +147,7 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
126147
totalsRow[7] = totals.UnrealizedPnLUSD
127148
totalsRow[8] = totals.STCGUSD
128149
totalsRow[9] = totals.LTCGUSD
129-
return cliio.WriteTableWithTotals(writer, headers, rows, totalsRow)
150+
return cliio.WriteTableWithTotals(writer, headers, sections, totalsRow)
130151
case cliio.FormatCSV:
131152
headers := ibctlholdings.HoldingsOverviewHeaders()
132153
records := make([][]string, 0, len(result.Holdings)+1)

cmd/ibctl/internal/ibctlcmd/ibctlcmd.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,11 @@ func NewDownloader(container appext.Container, configFilePath string) (ibctldown
3636
if ibkrToken == "" {
3737
return nil, errors.New(ibkrFlexWebServiceTokenEnvVar + " environment variable is required, set it to your IBKR Flex Web Service token (see \"ibctl --help\" for details)")
3838
}
39-
// Use the data directory from config.
40-
dataDirV1Path := config.DataDirV1Path
4139
// Extract the logger from the appext container.
4240
logger := container.Logger()
4341
// Construct the API clients.
4442
flexQueryClient := ibkrflexquery.NewClient(logger)
4543
fxRateClient := frankfurter.NewClient()
4644
bocClient := bankofcanada.NewClient()
47-
return ibctldownload.NewDownloader(logger, ibkrToken, dataDirV1Path, config, flexQueryClient, fxRateClient, bocClient), nil
45+
return ibctldownload.NewDownloader(logger, ibkrToken, config, flexQueryClient, fxRateClient, bocClient), nil
4846
}

internal/gen/proto/go/ibctl/data/v1/cash_position.pb.go

Lines changed: 145 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ibctl/ibctlconfig/ibctlconfig.go

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,22 @@ const configTemplate = `# The configuration file version.
2929
#
3030
# Required. The only current valid version is v1.
3131
version: v1
32-
# The data directory for ibctl to store downloaded and computed data.
32+
# The data directory for persistent trade data that accumulates over time.
3333
#
34-
# Required. A v1/ subdirectory will be created within this directory.
35-
data_dir: ~/Documents/ibctl
34+
# Required. Only contains trades.json per account, which is incrementally
35+
# merged across downloads. Cannot be safely deleted without losing history
36+
# beyond the 365-day API window.
37+
data_dir: ~/Documents/ibkr/data
38+
# The cache directory for downloaded snapshots (positions, FX rates, etc.).
39+
#
40+
# Required. Safe to delete — fully re-populated on the next download.
41+
cache_dir: ~/Documents/ibkr/cache
3642
# The Flex Query ID (visible next to your query name in the IBKR portal).
3743
#
3844
# Required. Create a Flex Query at https://www.interactivebrokers.com
3945
# under Performance & Reports > Flex Queries. Include the Trades,
40-
# Open Positions, and Cash Transactions sections with all fields enabled.
46+
# Open Positions, Cash Transactions, Cash Report, Transfers,
47+
# Trade Transfers, and Corporate Actions sections with all fields enabled.
4148
#
4249
# The Flex Web Service token must be set via the IBKR_FLEX_WEB_SERVICE_TOKEN environment variable.
4350
flex_query_id: ""
@@ -60,7 +67,7 @@ activity_statements_dir: ~/Documents/ibkr-statements
6067
# seed_dir: ~/Documents/ibkr/seed
6168
# Symbol classification configuration.
6269
#
63-
# Optional. Adds category, type, and sector metadata to holdings output.
70+
# Optional. Adds category, type, sector, and geo metadata to holdings output.
6471
# symbols:
6572
# - name: NET
6673
# category: EQUITY
@@ -73,8 +80,11 @@ activity_statements_dir: ~/Documents/ibkr-statements
7380
type ExternalConfigV1 struct {
7481
// Version is the configuration file version (must be "v1").
7582
Version string `yaml:"version"`
76-
// DataDir is the data directory for ibctl to store downloaded and computed data.
83+
// DataDir is the directory for persistent trade data that accumulates over time.
7784
DataDir string `yaml:"data_dir"`
85+
// CacheDir is the directory for downloaded snapshots (positions, FX rates, etc.).
86+
// Safe to delete — fully re-populated on the next download.
87+
CacheDir string `yaml:"cache_dir"`
7888
// FlexQueryID is the Flex Query ID.
7989
FlexQueryID string `yaml:"flex_query_id"`
8090
// ActivityStatementsDir is the directory containing IBKR Activity Statement CSVs.
@@ -105,19 +115,11 @@ type ExternalSymbolConfigV1 struct {
105115

106116
// Config is the validated runtime configuration derived from the config file.
107117
type Config struct {
108-
// DataDirV1Path is the resolved versioned data directory path (data_dir/v1).
109-
DataDirV1Path string
110-
// AccountsDirPath is the directory for per-account cached data (data_dir/v1/accounts).
111-
AccountsDirPath string
112-
// FXDirPath is the directory for FX rate data per currency pair (data_dir/v1/fx).
113-
FXDirPath string
118+
// DataDirPath is the resolved data directory path for persistent trade data.
119+
DataDirPath string
120+
// CacheDirPath is the resolved cache directory path for downloaded snapshots.
121+
CacheDirPath string
114122
// IBKRFlexQueryID is the Flex Query ID.
115-
//
116-
// To create a Flex Query, log in to IBKR Client Portal, navigate to
117-
// Performance & Reports > Flex Queries, and create a new query with
118-
// Trades, Open Positions, Cash Transactions, Transfers, Trade Transfers,
119-
// and Corporate Actions sections enabled.
120-
// The Query ID is displayed next to the query name in the list.
121123
IBKRFlexQueryID string
122124
// ActivityStatementsDirPath is the resolved path to the Activity Statements directory.
123125
ActivityStatementsDirPath string
@@ -152,6 +154,9 @@ func NewConfigV1(externalConfig ExternalConfigV1) (*Config, error) {
152154
if externalConfig.DataDir == "" {
153155
return nil, errors.New("data_dir is required")
154156
}
157+
if externalConfig.CacheDir == "" {
158+
return nil, errors.New("cache_dir is required")
159+
}
155160
if externalConfig.FlexQueryID == "" {
156161
return nil, errors.New("flex_query_id is required")
157162
}
@@ -177,12 +182,16 @@ func NewConfigV1(externalConfig ExternalConfigV1) (*Config, error) {
177182
accountAliases[alias] = accountID
178183
accountIDToAlias[accountID] = alias
179184
}
180-
// Resolve the data directory path and compute the v1 subdirectory.
185+
// Resolve the data directory path.
181186
dataDirPath, err := xos.ExpandHome(externalConfig.DataDir)
182187
if err != nil {
183188
return nil, err
184189
}
185-
dataDirV1Path := filepath.Join(dataDirPath, "v1")
190+
// Resolve the cache directory path.
191+
cacheDirPath, err := xos.ExpandHome(externalConfig.CacheDir)
192+
if err != nil {
193+
return nil, err
194+
}
186195
// Resolve the activity statements directory path.
187196
activityStatementsDirPath, err := xos.ExpandHome(externalConfig.ActivityStatementsDir)
188197
if err != nil {
@@ -213,9 +222,8 @@ func NewConfigV1(externalConfig ExternalConfigV1) (*Config, error) {
213222
}
214223
}
215224
return &Config{
216-
DataDirV1Path: dataDirV1Path,
217-
AccountsDirPath: filepath.Join(dataDirV1Path, "accounts"),
218-
FXDirPath: filepath.Join(dataDirV1Path, "fx"),
225+
DataDirPath: dataDirPath,
226+
CacheDirPath: cacheDirPath,
219227
IBKRFlexQueryID: externalConfig.FlexQueryID,
220228
ActivityStatementsDirPath: activityStatementsDirPath,
221229
SeedDirPath: seedDirPath,

0 commit comments

Comments
 (0)