Skip to content

Commit c3cd843

Browse files
Improvement: IBFlex stricter security matching
- If existing security has an ISIN, it must match - Currency must match the existing security (trades only) - Currency matching supports major/minor units
1 parent 7ecfd10 commit c3cd843

File tree

1 file changed

+51
-12
lines changed

1 file changed

+51
-12
lines changed

name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -392,15 +392,15 @@ private class IBFlexStatementExtractorResult
392392
case "Payment In Lieu Of Dividends":
393393
// Set the Symbol
394394
if (element.getAttribute("symbol").length() > 0)
395-
accountTransaction.setSecurity(this.getOrCreateSecurity(element, true));
395+
accountTransaction.setSecurity(this.getOrCreateSecurity(element, true, false));
396396

397397
accountTransaction.setType(AccountTransaction.Type.DIVIDENDS);
398398
this.calculateShares(accountTransaction, element);
399399
break;
400400
case "Withholding Tax":
401401
// Set the Symbol
402402
if (element.getAttribute("symbol").length() > 0)
403-
accountTransaction.setSecurity(this.getOrCreateSecurity(element, true));
403+
accountTransaction.setSecurity(this.getOrCreateSecurity(element, true, false));
404404

405405
// Positive amount are a tax refund
406406
if (Math.signum(Double.parseDouble(element.getAttribute("amount"))) == -1)
@@ -613,7 +613,7 @@ private class IBFlexStatementExtractorResult
613613
portfolioTransaction.setDate(extractDate(element));
614614

615615
// Set security before amount so that setAmount can detect currency mismatches in all cases
616-
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true));
616+
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true, true));
617617

618618
// @formatter:off
619619
// Set amount and check if the element contains the "netCash"
@@ -692,7 +692,7 @@ else if (Messages.MsgErrorOrderCancellationUnsupported.equals(portfolioTransacti
692692
double qty = Math.abs(Double.parseDouble(element.getAttribute("quantity")));
693693
portfolioTransaction.setShares(Values.Share.factorize(qty));
694694

695-
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true));
695+
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true, true));
696696

697697
portfolioTransaction.setMonetaryAmount(proceeds);
698698

@@ -714,7 +714,7 @@ else if (Messages.MsgErrorOrderCancellationUnsupported.equals(portfolioTransacti
714714
Double qty = Math.abs(Double.parseDouble(element.getAttribute("quantity")));
715715
portfolioTransaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
716716

717-
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true));
717+
portfolioTransaction.setSecurity(this.getOrCreateSecurity(element, true, true));
718718
portfolioTransaction.setNote(element.getAttribute("description"));
719719

720720
portfolioTransaction.setMonetaryAmount(proceeds);
@@ -1070,6 +1070,27 @@ else if (series.getBaseCurrency().equals(toCurrency) && series.getTermCurrency()
10701070
return null;
10711071
}
10721072

1073+
/**
1074+
* Checks if two currencies are compatible for matching purposes.
1075+
* Currencies are compatible if they are equal or if one is a major/minor
1076+
* unit of the other (e.g., GBP and GBX, ILS and ILA, ZAR and ZAC).
1077+
*
1078+
* @param currency1 First currency code
1079+
* @param currency2 Second currency code
1080+
* @return true if currencies are compatible
1081+
*/
1082+
private boolean isCurrencyCompatible(String currency1, String currency2)
1083+
{
1084+
if (currency1 == null || currency2 == null)
1085+
return false;
1086+
1087+
if (currency1.equals(currency2))
1088+
return true;
1089+
1090+
// Check if there's a fixed exchange rate between them (major/minor unit relationship)
1091+
return getUnitExchangeRate(currency1, currency2) != null;
1092+
}
1093+
10731094
/**
10741095
* @formatter:off
10751096
* Imports model objects from the statement based on the specified type using the provided handling function.
@@ -1199,11 +1220,13 @@ public void addError(Exception e)
11991220
* Looks up a Security in the model or creates a new one if it does not yet exist.
12001221
* It uses the IB ContractID (conID) for the WKN, tries to degrade if conID or ISIN are not available.
12011222
*
1202-
* @param element The XML element containing information about the security.
1203-
* @param doCreate A flag indicating whether to create a new Security if not found.
1204-
* @return The found or created Security object.
1223+
* @param element The XML element containing information about the security.
1224+
* @param doCreate A flag indicating whether to create a new Security if not found.
1225+
* @param strictCurrencyMatch If true (for trades), only match on ISIN if currency also matches.
1226+
* If false (for dividends), allow ISIN match regardless of currency.
1227+
* @return The found or created Security object.
12051228
*/
1206-
private Security getOrCreateSecurity(Element element, boolean doCreate)
1229+
private Security getOrCreateSecurity(Element element, boolean doCreate, boolean strictCurrencyMatch)
12071230
{
12081231
// Lookup the Exchange Suffix for Yahoo
12091232
Optional<String> tickerSymbol = Optional.ofNullable(element.getAttribute("symbol"));
@@ -1268,6 +1291,7 @@ private Security getOrCreateSecurity(Element element, boolean doCreate)
12681291
}
12691292

12701293
Security matchingSecurity = null;
1294+
Security matchingTickerSecurity = null;
12711295

12721296
for (Security security : allSecurities)
12731297
{
@@ -1276,18 +1300,33 @@ private Security getOrCreateSecurity(Element element, boolean doCreate)
12761300
return security;
12771301

12781302
if (!isin.isEmpty() && isin.equals(security.getIsin()))
1279-
if (currency.equals(security.getCurrencyCode()))
1303+
if (isCurrencyCompatible(currency, security.getCurrencyCode()))
12801304
return security;
1281-
else
1305+
else if (!strictCurrencyMatch)
12821306
matchingSecurity = security;
12831307

1308+
// Only match by ticker symbol if CONID and ISIN don't conflict
12841309
if (computedTickerSymbol.isPresent() && computedTickerSymbol.get().equals(security.getTickerSymbol()))
1285-
return security;
1310+
{
1311+
// Don't match by ticker if CONID or ISIN conflict
1312+
boolean conidConflicts = conid != null && conid.length() > 0
1313+
&& security.getWkn() != null && security.getWkn().length() > 0
1314+
&& !conid.equals(security.getWkn());
1315+
boolean isinConflicts = !isin.isEmpty()
1316+
&& security.getIsin() != null && security.getIsin().length() > 0
1317+
&& !isin.equals(security.getIsin());
1318+
1319+
if (!conidConflicts && !isinConflicts)
1320+
matchingTickerSecurity = security;
1321+
}
12861322
}
12871323

12881324
if (matchingSecurity != null)
12891325
return matchingSecurity;
12901326

1327+
if (matchingTickerSecurity != null)
1328+
return matchingTickerSecurity;
1329+
12911330
if (!doCreate)
12921331
return null;
12931332

0 commit comments

Comments
 (0)