Skip to content

Commit

Permalink
Add gross value in security currency for some IBFlex transactions
Browse files Browse the repository at this point in the history
Issue: #2805
Signed-off-by: csprunk <[email protected]>
[squashed commits; rebased to master]
Signed-off-by: Andreas Buchen <[email protected]>
  • Loading branch information
csprunk authored and buchen committed Apr 18, 2022
1 parent 90261d1 commit 0c0d28c
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<FlexQueryResponse queryName="PortfolioPerformence" type="AF">
<FlexStatements count="1">
<FlexStatement accountId="U1234567" fromDate="20180119" toDate="20180904" period="LastMonth" whenGenerated="20210904;114604">
<Trades>
<Trade accountId="U1234567" acctAlias="" model="" currency="EUR" fxRateToBase="1" assetCategory="STK" symbol="MMM" description="3M CO." conid="13098504" securityID="US88579Y1010" securityIDType="ISIN" cusip="" isin="US88579Y1010" listingExchange="FWB" underlyingConid="" underlyingSymbol="" underlyingSecurityID="" underlyingListingExchange="" issuer="" multiplier="1" strike="" expiry="" tradeID="2029054512" putCall="" reportDate="20180209" principalAdjustFactor="" dateTime="20180209;111929" tradeDate="20180209" settleDateTarget="20180213" transactionType="ExchTrade" exchange="TGATE" quantity="7" tradePrice="181.35" tradeMoney="1269.45" proceeds="-1269.45" taxes="0" ibCommission="-5.8" ibCommissionCurrency="EUR" netCash="-1275.25" closePrice="179" openCloseIndicator="O" notes="" cost="1275.25" fifoPnlRealized="0" fxPnl="0" mtmPnl="-16.45" origTradePrice="0" origTradeDate="" origTradeID="" origOrderID="0" clearingFirmID="" transactionID="8626274484" buySell="BUY" ibOrderID="999354200" ibExecID="00011b0b.aaa98f42.01.01" brokerageOrderID="" orderReference="" volatilityOrderLink="" exchOrderId="N/A" extExecID="88579Y101020180209161929225720/443524178" orderTime="20180209;111927" openDateTime="" holdingPeriodDateTime="" whenRealized="" whenReopened="" levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0" orderType="LMT" traderID="M5" isAPIOrder="N" accruedInt="0" serialNumber="" deliveryType="" commodityType="" fineness="0.0" weight="0.0 ()" />
<Trade accountId="U1234567" acctAlias="" model="" currency="USD" fxRateToBase="0.84807" assetCategory="STK" symbol="CDW" description="CDW CORP/DE" conid="130432552" securityID="US12514G1085" securityIDType="ISIN" cusip="12514G108" isin="US12514G1085" listingExchange="NASDAQ" underlyingConid="" underlyingSymbol="" underlyingSecurityID="" underlyingListingExchange="" issuer="" multiplier="1" strike="" expiry="" tradeID="3004185992" putCall="" reportDate="20200729" principalAdjustFactor="" dateTime="20200729;124452" tradeDate="20200729" settleDateTarget="20200731" transactionType="ExchTrade" exchange="IBKRATS" quantity="25" tradePrice="114.6" tradeMoney="2865" proceeds="-2865" taxes="0" ibCommission="-5" ibCommissionCurrency="USD" netCash="-2870" closePrice="115.64" openCloseIndicator="O" notes="" cost="2870" fifoPnlRealized="0" fxPnl="0" mtmPnl="26" origTradePrice="0" origTradeDate="" origTradeID="" origOrderID="0" clearingFirmID="" transactionID="13346288726" buySell="BUY" ibOrderID="1475852231" ibExecID="0000d323.5f2180e6.01.01" brokerageOrderID="000bb535.000193c2.5f20faa1.0005" orderReference="" volatilityOrderLink="" exchOrderId="N/A" extExecID="203807150B" orderTime="20200729;124452" openDateTime="" holdingPeriodDateTime="" whenRealized="" whenReopened="" levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0" orderType="LMT" traderID="M5" isAPIOrder="N" accruedInt="0" serialNumber="" deliveryType="" commodityType="" fineness="0.0" weight="0.0 ()" />
</Trades>
<CashTransactions>
<CashTransaction accountId="U1234567" acctAlias="" model="" currency="USD" fxRateToBase="0.8127" assetCategory="STK" symbol="MMM" description="MMM(US88579Y1010) CASH DIVIDEND 1.36000000 USD PER SHARE (Ordinary Dividend)" conid="13098504" securityID="US88579Y1010" securityIDType="ISIN" cusip="" isin="US88579Y1010" listingExchange="FWB" underlyingConid="" underlyingSymbol="" underlyingSecurityID="" underlyingListingExchange="" issuer="" multiplier="1" strike="" expiry="" putCall="" principalAdjustFactor="" dateTime="20180312;202000" settleDate="20180315" amount="9.52" type="Dividends" tradeID="" code="" transactionID="8765764573" reportDate="20180315" clientReference="" levelOfDetail="DETAIL" serialNumber="" deliveryType="" commodityType="" fineness="0.0" weight="0.0 ()" />
<CashTransaction accountId="U2379850" acctAlias="" model="" currency="USD" fxRateToBase="0.84642" assetCategory="STK" symbol="CDW" description="CDW(US12514G1085) CASH DIVIDEND USD 0.38 PER SHARE (Ordinary Dividend)" conid="130432552" securityID="US12514G1085" securityIDType="ISIN" cusip="12514G108" isin="US12514G1085" listingExchange="NASDAQ" underlyingConid="" underlyingSymbol="" underlyingSecurityID="" underlyingListingExchange="" issuer="" multiplier="1" strike="" expiry="" putCall="" principalAdjustFactor="" dateTime="20200910;202000" settleDate="20200910" amount="9.5" type="Dividends" tradeID="" code="" transactionID="13713058125" reportDate="20200910" clientReference="" levelOfDetail="DETAIL" serialNumber="" deliveryType="" commodityType="" fineness="0.0" weight="0.0 ()" />
</CashTransactions>
</FlexStatement>
</FlexStatements>
</FlexQueryResponse>
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package name.abuchen.portfolio.datatransfer;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.pdfbox.io.IOUtils;
import org.junit.Test;

import name.abuchen.portfolio.datatransfer.Extractor.BuySellEntryItem;
import name.abuchen.portfolio.datatransfer.Extractor.Item;
import name.abuchen.portfolio.datatransfer.Extractor.SecurityItem;
import name.abuchen.portfolio.datatransfer.Extractor.TransactionItem;
import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.BuySellEntry;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Transaction.Unit;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Quote;
import name.abuchen.portfolio.money.Values;

@SuppressWarnings("nls")
public class IBFlexStatementExtractorWithForeignDividendNoAccountInfoTest
{

private List<Item> runExtractor(List<Exception> errors) throws IOException
{
Client client = new Client();
// We add two securities to the client with EUR as currency, both will
// receive dividends in USD.
Security security = new Security("3M CO. already defined", CurrencyUnit.EUR);
security.setIsin("US88579Y1010");
client.addSecurity(security);

security = new Security("CDW CORP/DE already defined", CurrencyUnit.EUR);
security.setIsin("US12514G1085");
client.addSecurity(security);

InputStream activityStatement = getClass()
.getResourceAsStream("IBActivityStatementWithForeignDividendNoAccountInfo.xml");
Extractor.InputFile tempFile = createTempFile(activityStatement);
IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client);

return extractor.extract(Collections.singletonList(tempFile), errors);
}

@Test
public void testIBAcitvityStatement() throws IOException
{
List<Exception> errors = new ArrayList<>();
List<Item> results = runExtractor(errors);
assertThat(errors.isEmpty(), is(true));
int numSecurity = 0; // The two securities are already present in the
// client.
int numBuySell = 2;
int numTransactions = 2;

results.stream().filter(i -> !(i instanceof SecurityItem))
.forEach(i -> assertThat(i.getAmount(), notNullValue()));

List<Extractor.Item> securityItems = results.stream().filter(SecurityItem.class::isInstance)
.collect(Collectors.toList());

assertThat(securityItems.size(), is(numSecurity));

List<Extractor.Item> buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance)
.collect(Collectors.toList());

assertThat(buySellTransactions.size(), is(numBuySell));

List<Extractor.Item> accountTransactions = results.stream().filter(TransactionItem.class::isInstance)
.collect(Collectors.toList());

assertThat(accountTransactions.size(), is(numTransactions));

assertThat(results.size(), is(numSecurity + numBuySell + numTransactions));

assertFirstBuySell(results.stream().filter(BuySellEntryItem.class::isInstance).findFirst());
assertFirstTransaction(results.stream().filter(TransactionItem.class::isInstance).findFirst());
assertSecondTransaction(results.stream().filter(TransactionItem.class::isInstance).skip(1).findFirst());
}

private void assertFirstBuySell(Optional<Item> item)
{
assertThat(item.isPresent(), is(true));
assertThat(item.orElseThrow().getSubject(), instanceOf(BuySellEntry.class));
BuySellEntry entry = (BuySellEntry) item.orElseThrow().getSubject();

assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY));
assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY));

assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("3M CO. already defined"));
assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), is(Money.of("EUR", 1275_25L)));
assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2018-02-09T11:19")));
assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(7)));
assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), is(Money.of("EUR", 5_80L)));

assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(),
is(Quote.of("EUR", Values.Quote.factorize(181.35))));
}

private void assertFirstTransaction(Optional<Item> item)
{
assertThat(item.isPresent(), is(true));
assertThat(item.orElseThrow().getSubject(), instanceOf(AccountTransaction.class));
AccountTransaction entry = (AccountTransaction) item.orElseThrow().getSubject();

assertThat(entry.getType(), is(AccountTransaction.Type.DIVIDENDS));

assertThat(entry.getSecurity().getName(), is("3M CO. already defined"));
assertThat(entry.getSecurity().getIsin(), is("US88579Y1010"));
assertThat(entry.getMonetaryAmount(), is(Money.of("USD", 9_52L)));
assertThat(entry.getCurrencyCode(), is("USD"));
assertThat(entry.getSecurity().getCurrencyCode(), is("EUR"));
Unit grossValue = entry.getUnit(Unit.Type.GROSS_VALUE).orElseThrow();
assertThat(grossValue.getForex(), is(Money.of("EUR", 7_74L)));
assertThat(grossValue.getAmount(), is(Money.of("USD", 9_52L)));
}

private void assertSecondTransaction(Optional<Item> item)
{
assertThat(item.isPresent(), is(true));
assertThat(item.orElseThrow().getSubject(), instanceOf(AccountTransaction.class));
AccountTransaction entry = (AccountTransaction) item.orElseThrow().getSubject();

assertThat(entry.getType(), is(AccountTransaction.Type.DIVIDENDS));

assertThat(entry.getSecurity().getName(), is("CDW CORP/DE already defined"));
assertThat(entry.getSecurity().getIsin(), is("US12514G1085"));
assertThat(entry.getMonetaryAmount(), is(Money.of("USD", 9_50L)));
assertThat(entry.getCurrencyCode(), is("USD"));
assertThat(entry.getSecurity().getCurrencyCode(), is("EUR"));
Unit grossValue = entry.getUnit(Unit.Type.GROSS_VALUE).orElseThrow();
assertThat(grossValue.getForex(), is(Money.of("EUR", 8_04L)));
assertThat(grossValue.getAmount(), is(Money.of("USD", 9_50L)));
}

private Extractor.InputFile createTempFile(InputStream input) throws IOException
{
File tempFile = File.createTempFile("iBFlexStatementExtractorTest", null);
FileOutputStream fos = new FileOutputStream(tempFile);

IOUtils.copy(input, fos);
return new Extractor.InputFile(tempFile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,9 @@ private void setAmount(Element element, Transaction transaction, Double amount,
{
fxRateToBase = new BigDecimal(1);
}
BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN);

BigDecimal baseCurrencyMoney = BigDecimal.valueOf(amount.doubleValue() * Values.Amount.factor())
.divide(inverseRate, RoundingMode.HALF_DOWN);
.multiply(fxRateToBase);
transaction.setAmount(Math.round(baseCurrencyMoney.doubleValue()));
transaction.setCurrencyCode(ibAccountCurrency);
if (addUnit)
Expand All @@ -532,6 +531,45 @@ private void setAmount(Element element, Transaction transaction, Double amount,
{
transaction.setAmount(Math.round(amount.doubleValue() * Values.Amount.factor()));
transaction.setCurrencyCode(currency);

if (addUnit && transaction.getSecurity() != null
&& !transaction.getSecurity().getCurrencyCode().equals(currency))
{
// 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).
String fxRateToBaseString = element.getAttribute("fxRateToBase");
BigDecimal fxRateToBase;
if (fxRateToBaseString != null && !fxRateToBaseString.isEmpty())
{
fxRateToBase = BigDecimal.valueOf(Double.parseDouble(fxRateToBaseString));
}
else
{
fxRateToBase = new BigDecimal(1);
}
// To back out the amount in the security currency we could
// multiply with fxRateToBase. Instead, we calculate the
// inverse rate and divide by it as we need to supply the
// inverse rate for the gross value below (which converts
// from security currency to original
// transaction currency).
BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN);

BigDecimal securityCurrencyMoney = BigDecimal.valueOf(amount.doubleValue() * Values.Amount.factor())
.divide(inverseRate, RoundingMode.HALF_DOWN);

// Gross value with conversion information for the security
// currency.
Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, transaction.getMonetaryAmount(),
Money.of(transaction.getSecurity().getCurrencyCode(),
Math.round(securityCurrencyMoney.doubleValue())),
inverseRate);
transaction.addUnit(grossValue);
}

}
}

Expand Down

0 comments on commit 0c0d28c

Please sign in to comment.