Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ private static DateTimeFormatter createFormatter(String pattern)
.toFormatter(Locale.US);
}

// Map to store currency conversion rates by (date, "fromCurrency-toCurrency")
private Map<Pair<String, String>, BigDecimal> conversionRates = new HashMap<>();

/**
* Constructs an IBFlexStatementExtractor with the given client.
* Initializes the list of securities and exchange mappings.
Expand Down Expand Up @@ -225,6 +228,24 @@ private class IBFlexStatementExtractorResult
private List<Item> results = new ArrayList<>();
private String accountCurrency = null;

/**
* Processes ConversionRate elements to build currency conversion rate mapping.
* Stores exchange rates from each currency to the account base currency.
*/
private Consumer<Element> buildConversionRates = element -> {
String reportDate = element.getAttribute("reportDate");
String toCurrency = element.getAttribute("toCurrency");
String fromCurrency = element.getAttribute("fromCurrency");
String rateStr = element.getAttribute("rate");

if (!rateStr.equals("-1"))
{
BigDecimal rate = asExchangeRate(rateStr);
Pair<String, String> key = new Pair<>(reportDate, fromCurrency + "-" + toCurrency);
conversionRates.put(key, rate);
}
};

/**
* Builds account information based on the provided XML element. Extracts the currency
* attribute from the element, converts it to a currency code, and sets the corresponding
Expand Down Expand Up @@ -710,20 +731,18 @@ private Unit createUnit(Element element, Unit.Type unitType, Money amount)
}
else
{
// Calculate the FX rate to the base currency
BigDecimal fxRateToBase = element.getAttribute("fxRateToBase").isEmpty() ? BigDecimal.ONE
: asExchangeRate(element.getAttribute("fxRateToBase"));
BigDecimal exchangeRate = getExchangeRate(element, amount.getCurrencyCode(), accountCurrency);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe getExchangeRate can return null.

As I understand it, getExchangeRate is also checking for fxRateToBase and it now attempts to calculate the cross rate using the account currency, to probably it is not a change. But maybe a check for null could make sense


// Calculate the inverse rate
BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN);
BigDecimal inverseRate = BigDecimal.ONE.divide(exchangeRate, 10, RoundingMode.HALF_DOWN);

// Convert the amount to the IB account currency using the
// inverse rate
Money fxAmount = Money.of(accountCurrency, BigDecimal.valueOf(amount.getAmount())
.divide(inverseRate, Values.MC).setScale(0, RoundingMode.HALF_UP).longValue());

// Create a Unit with FX amount, original amount, and FX rate
unit = new Unit(unitType, fxAmount, amount, fxRateToBase);
unit = new Unit(unitType, fxAmount, amount, exchangeRate);
}

return unit;
Expand Down Expand Up @@ -753,17 +772,16 @@ private void setAmount(Element element, Transaction transaction, Money amount, b
{
if (accountCurrency != null && !accountCurrency.equals(amount.getCurrencyCode()))
{
BigDecimal fxRateToBase = element.getAttribute("fxRateToBase").isEmpty() ? BigDecimal.ONE
: asExchangeRate(element.getAttribute("fxRateToBase"));
BigDecimal exchangeRate = getExchangeRate(element, amount.getCurrencyCode(), accountCurrency);

Money fxAmount = Money.of(accountCurrency, BigDecimal.valueOf(amount.getAmount())
.multiply(fxRateToBase).setScale(0, RoundingMode.HALF_UP).longValue());
.multiply(exchangeRate).setScale(0, RoundingMode.HALF_UP).longValue());

transaction.setMonetaryAmount(fxAmount);

if (addUnit)
{
Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, fxAmount, amount, fxRateToBase);
Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, fxAmount, amount, exchangeRate);
transaction.addUnit(grossValue);
}
}
Expand All @@ -776,23 +794,19 @@ private void setAmount(Element element, Transaction transaction, Money amount, b
// @formatter:off
// If the transaction currency is different from the security currency (as stored in PP)
// we need to supply the gross value in the security currency.
//
// We assume that the security currency is the same that IB
// thinks of as base currency for this transaction (fxRateToBase).
// @formatter:on
BigDecimal fxRateToBase = element.getAttribute("fxRateToBase").isEmpty() ? BigDecimal.ONE
: asExchangeRate(element.getAttribute("fxRateToBase"));
BigDecimal exchangeRate = getExchangeRate(element, amount.getCurrencyCode(), transaction.getSecurity().getCurrencyCode());

BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN);
if (exchangeRate != null) {
BigDecimal inverseRate = BigDecimal.ONE.divide(exchangeRate, 10, RoundingMode.HALF_DOWN);

Money fxAmount = Money.of(transaction.getSecurity().getCurrencyCode(),
BigDecimal.valueOf(amount.getAmount()).divide(inverseRate, Values.MC)
.setScale(0, RoundingMode.HALF_UP).longValue());
Money fxAmount = Money.of(transaction.getSecurity().getCurrencyCode(),
BigDecimal.valueOf(amount.getAmount()).divide(inverseRate, Values.MC)
.setScale(0, RoundingMode.HALF_UP).longValue());

transaction.setMonetaryAmount(amount);

Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, amount, fxAmount, inverseRate);
transaction.addUnit(grossValue);
Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, amount, fxAmount, inverseRate);
transaction.addUnit(grossValue);
}
}
}
}
Expand Down Expand Up @@ -882,6 +896,68 @@ private String extractNote(Element element)
return note.length() > 0 ? note.toString() : null;
}

/**
* Gets the exchange rate to go from fromCurrency to toCurrency.
*
* @param element The XML element containing transaction details
* @param fromCurrency The currency to convert from
* @param toCurrency The currency to convert to
* @return The exchange rate, or null if it cannot be determined
*/
private BigDecimal getExchangeRate(Element element, String fromCurrency, String toCurrency)
{
if (fromCurrency.equals(toCurrency))
return BigDecimal.ONE;

LocalDateTime dateTime = extractDate(element);
if (dateTime == null)
{
return null;
}

String dateStr = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));

// Attempt a direct lookup in the rates table.
Pair<String, String> key = new Pair<>(dateStr, fromCurrency + "-" + toCurrency);
BigDecimal rate = conversionRates.get(key);
if (rate != null)
{
return rate;
}

key = new Pair<>(dateStr, toCurrency + "-" + fromCurrency);
rate = conversionRates.get(key);
if (rate != null)
return ExchangeRate.inverse(rate);

// Fall back to using accountCurrency as an intermediate.
if (accountCurrency != null)
{
if (toCurrency.equals(accountCurrency) && element.hasAttribute("fxRateToBase"))
{
// Avoid cross rate if possible by using fxRateToBase from the
// transaction element itself.
return asExchangeRate(element.getAttribute("fxRateToBase"));
}

// Attempt to calculate cross rate via accountCurrency. No use
// in trying a different intermediate currency, it seems like
// toCurrency is only ever the account's base.
Pair<String, String> fromKey = new Pair<>(dateStr, fromCurrency + "-" + accountCurrency);
Pair<String, String> toKey = new Pair<>(dateStr, toCurrency + "-" + accountCurrency);

BigDecimal fromRate = conversionRates.get(fromKey);
BigDecimal toRate = conversionRates.get(toKey);

if (fromRate != null && toRate != null)
{
return fromRate.divide(toRate, 10, RoundingMode.HALF_DOWN);
}
}

return null;
}

/**
* @formatter:off
* Imports model objects from the document based on the specified type using the provided handling function.
Expand Down Expand Up @@ -930,6 +1006,9 @@ public void parseDocument(Document doc)
if (document == null)
return;

// Process conversion rates first
importModelObjects("ConversionRate", buildConversionRates);

// Import AccountInformation
importModelObjects("AccountInformation", buildAccountInformation);

Expand All @@ -948,7 +1027,7 @@ public void parseDocument(Document doc)
// Process all SalesTaxes
importModelObjects("SalesTax", buildSalesTaxTransaction);

// TODO: Process all FxTransactions and ConversionRates
// TODO: Process all FxTransactions
}

public void addError(Exception e)
Expand Down