IBFlex: Import Flex Queries via HTTP API#5352
IBFlex: Import Flex Queries via HTTP API#5352lmb wants to merge 1 commit intoportfolio-performance:masterfrom
Conversation
|
@buchen is this something you would consider merging? Happy to try and slim this down more if it is too much to review. |
d2bbc0c to
0bf0746
Compare
0bf0746 to
6af9bb5
Compare
|
Sorry @lmb it was just not top on my mind (and a lot of other pull requests have run up). Besides that, I haven't looked in detail but hope to be able to do so over public holidays. |
There was a problem hiding this comment.
Pull request overview
Implements Interactive Brokers Flex Web Service (HTTP API) integration to fetch Flex Query statements directly from IBKR and import new transactions into Portfolio Performance, including UI for configuring credentials and tracking last import time.
Changes:
- Added
IBFlexWebServiceClientto request and retrieve Flex statements via the IBKR Flex Web Service with retry/cancellation handling. - Extended
IBFlexStatementExtractorto optionally filter extracted items by a cutoff date (used for incremental imports). - Added UI commands/handlers, configuration dialog, and persisted client properties (token/queryId/lastImportDate), plus supporting i18n strings and tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties | Adds core (non-UI) IB Flex Web Service error strings. |
| name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java | Exposes new core message keys. |
| name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexWebServiceClient.java | New HTTP client for IBKR Flex Web Service (send request + get statement) with retries and XML parsing. |
| name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java | Adds optional import-date cutoff filtering to support incremental imports. |
| name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties | Adds UI strings for IB Flex configuration/import UX. |
| name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java | Exposes new UI message keys. |
| name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/OpenIBFlexSettingsHandler.java | Adds handler to open IB Flex settings dialog for active client. |
| name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/ImportIBFlexWebServiceHandler.java | Adds handler to fetch statement via web service, extract/import items, and persist last import date. |
| name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/dialogs/IBFlexConfigurationDialog.java | New dialog/model to store token/query ID and show/clear last import date. |
| name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/dialogs/IBFlexModelTest.java | Tests for IBFlexModel property persistence and lastImportDate round-tripping. |
| name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexWebServiceClientTest.java | Unit tests for web service client parsing and basic fetch flow with mock executor. |
| name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorTest.java | Adds tests validating cutoff-date filtering behavior. |
| name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle.properties | Adds menu/command labels for the new Flex Web Service import/settings actions (EN). |
| name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle_de.properties | Adds menu/command labels for the new Flex Web Service import/settings actions (DE). |
| name.abuchen.portfolio.bootstrap/Application.e4xmi | Wires new menu entries, commands, and handlers into the Eclipse e4 application model. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| link.addListener(SWT.Selection, e -> DesktopAPI.browse( | ||
| "https://www.interactivebrokers.com/campus/ibkr-api-page/flex-web-service/")); //$NON-NLS-1$ | ||
| } | ||
|
|
There was a problem hiding this comment.
The browse handler hardcodes the documentation URL instead of reusing the value shown to the user (Messages.IBFlexDocumentationLink). This can lead to inconsistencies (e.g., if the link text is translated/changed but the opened URL is not). Consider deriving the URL from the message (or storing the URL separately as a constant) and using the same source for both display and navigation.
| link.addListener(SWT.Selection, e -> DesktopAPI.browse( | |
| "https://www.interactivebrokers.com/campus/ibkr-api-page/flex-web-service/")); //$NON-NLS-1$ | |
| } | |
| link.addListener(SWT.Selection, e -> DesktopAPI.browse(Messages.IBFlexDocumentationLink)); | |
| } |
| String message = lastImportDate != null | ||
| ? MessageFormat.format(Messages.IBFlexNoNewItemsExtracted, lastImportDate) | ||
| : Messages.IBFlexNoItemsExtracted; |
There was a problem hiding this comment.
The "no new items" info message formats {0} with a raw LocalDateTime (lastImportDate), which will render in ISO form (e.g., 2025-01-01T00:00) and may not match the UI’s usual date/time formatting. Consider formatting lastImportDate with the same formatter used elsewhere in the dialog (or a shared UI date-time format) before passing it to MessageFormat.
6af9bb5 to
0bf497a
Compare
IBKR allows retrieving preconfigured Flex Queries via a rudimentary HTTP interface. Doing so requires a query ID which identifies the Flex Query, and a secret token. The secret token can be obtained from the web interface an expires after a while. Add a new menu item which allows importing from such a Flex Web Service. This saves users from logging in, manually selecting the query, executing it, etc. The Flex Web Service always runs a Flex Query with a fixed date range configured when creating the query. This means that we can't choose what date range to fetch. The easiest thing for the user is to configure the query with the maximum duration (currently 365 days). The downside of this is that it populates the import wizard with a lot of duplicate items after the first import. Filter extracted items to exclude ones we've likely already imported based on the transaction date. The user can clear the date we're using for this via the settings dialog.
0bf497a to
938b288
Compare
|
@buchen reworked this to use a new preference dialog to store tokens + query cutoff times. Also improved cancellation of the web fetch. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private static TreeMap<String, String> loadLastImportDates(Client client) | ||
| { | ||
| var json = client.getProperty(PROPERTY_LAST_IMPORT_DATES); | ||
| if (json == null || json.isBlank()) | ||
| return new TreeMap<>(); | ||
|
|
||
| Type mapType = new TypeToken<TreeMap<String, String>>() | ||
| { | ||
| }.getType(); | ||
| TreeMap<String, String> result = GSON.fromJson(json, mapType); | ||
| return result != null ? result : new TreeMap<>(); | ||
| } |
There was a problem hiding this comment.
IBFlexConfigurationDialog.loadLastImportDates() calls GSON.fromJson() without handling malformed/corrupted JSON stored in the client property. A JsonSyntaxException here would propagate into UI code and can break importing / preference pages. Consider catching runtime JSON parsing errors (e.g., JsonSyntaxException), logging once, and returning an empty TreeMap as a safe fallback.
| public static List<Credential> deserialize(String value) | ||
| { | ||
| if (value == null || value.isBlank()) | ||
| return new ArrayList<>(); | ||
|
|
||
| Type listType = new TypeToken<ArrayList<Credential>>() | ||
| { | ||
| }.getType(); | ||
| List<Credential> result = GSON.fromJson(value, listType); | ||
| return result != null ? result : new ArrayList<>(); | ||
| } |
There was a problem hiding this comment.
IBFlexConfigurationDialog.deserialize() calls GSON.fromJson() without guarding against invalid JSON in the preference value (e.g., user-edited/corrupted workspace prefs). This can throw JsonSyntaxException during menu enablement/visibility evaluation and break the UI. Suggest catching JSON parsing exceptions, logging, and returning an empty list as a fallback.
| String message = lastImportDate != null | ||
| ? MessageFormat.format(Messages.IBFlexNoNewItemsExtracted, lastImportDate) | ||
| : Messages.IBFlexNoItemsExtracted; | ||
| MessageDialog.openInformation(shell, Messages.LabelInfo, message); |
There was a problem hiding this comment.
The user-facing message for “no new items” formats lastImportDate via MessageFormat with a raw LocalDateTime argument, which will render in ISO form (e.g., 2025-01-01T00:00) and is inconsistent with other UI date/time formatting. Consider formatting lastImportDate with the existing UI formatter (e.g., Values.DateTime.format(...)) before inserting it into the message.
| var dialog = new MessageDialog(shell, Messages.IBFlexFetching, null, | ||
| Messages.IBFlexQueryId, MessageDialog.QUESTION, 0, labels); |
There was a problem hiding this comment.
selectCredential() uses Messages.IBFlexFetching as the dialog title and Messages.IBFlexQueryId as the dialog message. This results in a confusing question dialog (title implies an in-progress fetch, message is just “Query ID”). Consider introducing a dedicated message like “Select which Flex Query to import” and using a stable title (e.g., the command name / “Interactive Brokers”).
| var dialog = new MessageDialog(shell, Messages.IBFlexFetching, null, | |
| Messages.IBFlexQueryId, MessageDialog.QUESTION, 0, labels); | |
| var dialog = new MessageDialog(shell, "Interactive Brokers", null, | |
| "Select which Flex Query to import", MessageDialog.QUESTION, 0, labels); |
| // Exponential backoff, capped at MAX_DELAY_MS | ||
| delay = Math.min(delay * 2, MAX_DELAY_MS); | ||
| } | ||
| } | ||
|
|
||
| throw new IBFlexException(ERROR_STATEMENT_GENERATING, Messages.IBFlexMsgErrorStatementNotReady); | ||
| } |
There was a problem hiding this comment.
fetchStatementWithRetry() throws an IBFlexException with errorCode ERROR_STATEMENT_GENERATING (1019) when the local retry loop times out. Because isStatementGenerating() is derived from the errorCode, callers may treat a local timeout as a retriable “still generating” response and keep retrying indefinitely. Consider using a distinct local error code (or a dedicated flag/exception subtype) for the timeout case so isStatementGenerating() remains accurate.
This is an implementation of the Flex Web Service API to automatically fetch Flex Queries. Please see the individual commit descriptions for details.
Disclosure: I used Claude Code to draft this PR and have done manual code review on it. I think the code is OK, maybe a bit verbose. Please let me know if I've missed something or if I can somehow remove some code.
Fixes: #5246