From f64aa6c953a2efe0d7cb1a85d235040ec89295a1 Mon Sep 17 00:00:00 2001 From: Nirus2000 Date: Fri, 10 Feb 2023 18:02:21 +0100 Subject: [PATCH] Revision of the complete IB Flex query importer Fixes #2420 Fixes #2139 Fixes #3067 Fixes #3095 https://forum.portfolio-performance.info/t/interactive-brokers/276/122 https://forum.portfolio-performance.info/t/interactive-brokers-ungultige-buchungskomponente-fee/9411/2 https://forum.portfolio-performance.info/t/interactive-brokers/276/122 .... and more Integration in the ExtractorUtils.java Refactoring tests, test cases and structure Completed all missing tests and test cases and other thinks... --- .../ibflex/IBFlexStatementExtractorTest.java | 2162 ++++++++++++++++- ...tementExtractorWithAccountDetailsTest.java | 189 -- ...xtractorWithDuplicateTransactionsTest.java | 112 - ...ExtractorWithFixGrossValueBuySellTest.java | 122 - ...rWithForeignDividendNoAccountInfoTest.java | 162 -- ...ementExtractorWithForeignDividendTest.java | 158 -- ...ment.xml => testiBFlexStatementFile01.xml} | 0 ...ails.xml => testiBFlexStatementFile02.xml} | 0 ...ions.xml => testiBFlexStatementFile03.xml} | 0 ...Sell.xml => testiBFlexStatementFile04.xml} | 0 ...Info.xml => testiBFlexStatementFile05.xml} | 0 ...dend.xml => testiBFlexStatementFile06.xml} | 0 .../ibflex/testiBFlexStatementFile07.xml | 9 + .../ibflex/testiBFlexStatementFile08.xml | 11 + .../ibflex/testiBFlexStatementFile09.xml | 10 + .../ibflex/testiBFlexStatementFile10.xml | 13 + .../ibflex/testiBFlexStatementFile11.xml | 12 + .../ibflex/testiBFlexStatementFile12.xml | 29 + .../ibflex/testiBFlexStatementFile13.xml | 9 + .../datatransfer/ExtractorUtils.java | 4 +- .../ibflex/IBFlexStatementExtractor.java | 526 ++-- 21 files changed, 2433 insertions(+), 1095 deletions(-) delete mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithAccountDetailsTest.java delete mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithDuplicateTransactionsTest.java delete mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithFixGrossValueBuySellTest.java delete mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendNoAccountInfoTest.java delete mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendTest.java rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatement.xml => testiBFlexStatementFile01.xml} (100%) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatementWithAccountDetails.xml => testiBFlexStatementFile02.xml} (100%) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatementWithDuplicateTransactions.xml => testiBFlexStatementFile03.xml} (100%) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatementWithFixGrossValueBuySell.xml => testiBFlexStatementFile04.xml} (100%) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatementWithForeignDividendNoAccountInfo.xml => testiBFlexStatementFile05.xml} (100%) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/{IBActivityStatementWithForeignDividend.xml => testiBFlexStatementFile06.xml} (100%) create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile07.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile08.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile09.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile10.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile11.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile12.xml create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile13.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorTest.java index c3ec4e588f..880688de73 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorTest.java @@ -1,9 +1,10 @@ package name.abuchen.portfolio.datatransfer.ibflex; -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 static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.junit.Assert.assertNull; import java.io.File; import java.io.FileOutputStream; @@ -12,225 +13,2116 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; 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.Messages; import name.abuchen.portfolio.datatransfer.Extractor; import name.abuchen.portfolio.datatransfer.Extractor.BuySellEntryItem; import name.abuchen.portfolio.datatransfer.Extractor.Item; +import name.abuchen.portfolio.datatransfer.Extractor.NonImportableItem; import name.abuchen.portfolio.datatransfer.Extractor.SecurityItem; import name.abuchen.portfolio.datatransfer.Extractor.TransactionItem; +import name.abuchen.portfolio.datatransfer.actions.AssertImportActions; import name.abuchen.portfolio.model.AccountTransaction; -import name.abuchen.portfolio.model.AccountTransaction.Type; 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; +import name.abuchen.portfolio.online.impl.AlphavantageQuoteFeed; +import name.abuchen.portfolio.online.impl.YahooFinanceQuoteFeed; @SuppressWarnings("nls") public class IBFlexStatementExtractorTest { + 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); + } + + @Test + public void testThatExceptionIsAddedForNonFlexStatementDocuments() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream otherFile = getClass().getResourceAsStream("../pdf/comdirect/Dividende05.txt"); + Extractor.InputFile tempFile = createTempFile(otherFile); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + assertThat(results.isEmpty(), is(true)); + assertThat(errors.size(), is(1)); + } + + @Test + public void testiBFlexStatementFile01() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile01.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + // Filter securities + results.stream().filter(i -> !(i instanceof SecurityItem)) + .forEach(i -> assertThat(i.getAmount(), notNullValue())); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(5)); + assertThat(buySellTransactions.size(), is(15)); + assertThat(accountTransactions.size(), is(11)); + assertThat(results.size(), is(31)); + + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("CA38501D2041")); + assertThat(security1.getWkn(), is("80845553")); + assertThat(security1.getTickerSymbol(), is("GCM.TO")); + assertThat(security1.getName(), is("GRAN COLOMBIA GOLD CORP")); + assertThat(security1.getCurrencyCode(), is("CAD")); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security2 = results.stream().filter(SecurityItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security2.getIsin(), is("CA91701P1053")); + assertThat(security2.getWkn(), is("44924734")); + assertThat(security2.getTickerSymbol(), is("UUU.TO")); + assertThat(security2.getName(), is("URANIUM ONE INC.")); + assertThat(security2.getCurrencyCode(), is("CAD")); + assertThat(security2.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security3 = results.stream().filter(SecurityItem.class::isInstance).skip(2).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security3.getIsin(), is("")); + assertThat(security3.getWkn(), is("277684800")); + assertThat(security3.getTickerSymbol(), is("FB180921C00200000")); + assertThat(security3.getName(), is("FB 21SEP18 200.0 C")); + assertThat(security3.getCurrencyCode(), is("CAD")); + assertThat(security3.getFeed(), is(YahooFinanceQuoteFeed.ID)); + + Security security4 = results.stream().filter(SecurityItem.class::isInstance).skip(3).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security4.getIsin(), is("CA91701P1TN3")); + assertThat(security4.getWkn(), is("123720813")); + assertThat(security4.getTickerSymbol(), is("UUU.TEN2")); + assertThat(security4.getName(), is("UUU(CA91701P1053) TENDERED TO CA91701P1TN3 1 FOR 1 (UUU.TEN2, URANIUM ONE INC. - TENDER FOR CASH CAD, CA91701P1TN3)")); + assertThat(security4.getCurrencyCode(), is("CAD")); + assertThat(security4.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security5 = results.stream().filter(SecurityItem.class::isInstance).skip(4).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security5.getIsin(), is("CA38501D5010")); + assertThat(security5.getWkn(), is("129258970")); + assertThat(security5.getTickerSymbol(), is("GCM")); + assertThat(security5.getName(), is("GCM(CA38501D2041) SPLIT 1 FOR 25 (GCM, GRAN COLOMBIA GOLD CORP, CA38501D5010)")); + assertThat(security5.getCurrencyCode(), is("CAD")); + assertThat(security5.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check transaction + // get transactions + Iterator iter = results.stream().filter(TransactionItem.class::isInstance).iterator(); + assertThat(results.stream().filter(TransactionItem.class::isInstance).count(), is(11L)); + + Item item = iter.next(); + + // assert transaction + AccountTransaction transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2013-01-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of("CHF", Values.Amount.factorize(10.00)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("VIRTX/EBS (NP,L1) FOR DEC 2012")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2013-04-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of("CHF", Values.Amount.factorize(9.49)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("BALANCE OF MONTHLY MINIMUM FEE FOR MAR 2013")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.INTEREST)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2013-02-05T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of("CAD", Values.Amount.factorize(1.23)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("CAD DEBIT INT FOR JAN-2013")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.INTEREST_CHARGE)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2013-02-05T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of("CAD", Values.Amount.factorize(15.17)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("CAD DEBIT INT FOR JAN-2013")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.TAXES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-09-15T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(2.07)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("GCM.TO CASH DIVIDEND USD 0.6900000000 - US TAX")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-05-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.18)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("NP SECURITIES AND FUTURES VALUE BUNDLE FOR APR 2017")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES_REFUND)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-05-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.18)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("NP SECURITIES AND FUTURES VALUE BUNDLE FOR APR 2017")); + + // check delivery outbound (Auslieferung) transaction + PortfolioTransaction transaction1 = (PortfolioTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(7).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction1.getType(), is(PortfolioTransaction.Type.DELIVERY_OUTBOUND)); + + assertThat(transaction1.getDateTime(), is(LocalDateTime.parse("2013-03-05T00:00"))); + assertThat(transaction1.getShares(), is(Values.Share.factorize(12000))); + assertNull(transaction1.getSource()); + assertThat(transaction1.getNote(), is("UUU(CA91701P1053) TENDERED TO CA91701P1TN3 1 FOR 1 (UUU, URANIUM ONE INC., CA91701P1053)")); + + assertThat(transaction1.getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + + // check delivery inbound (Einlieferung) transaction + transaction1 = (PortfolioTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(8).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction1.getType(), is(PortfolioTransaction.Type.DELIVERY_INBOUND)); + + assertThat(transaction1.getDateTime(), is(LocalDateTime.parse("2013-03-05T00:00"))); + assertThat(transaction1.getShares(), is(Values.Share.factorize(12000))); + assertNull(transaction1.getSource()); + assertThat(transaction1.getNote(), is("UUU(CA91701P1053) TENDERED TO CA91701P1TN3 1 FOR 1 (UUU.TEN2, URANIUM ONE INC. - TENDER FOR CASH CAD, CA91701P1TN3)")); + + assertThat(transaction1.getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + + // check delivery inbound (Einlieferung) transaction + transaction1 = (PortfolioTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(9).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction1.getType(), is(PortfolioTransaction.Type.DELIVERY_INBOUND)); + + assertThat(transaction1.getDateTime(), is(LocalDateTime.parse("2013-06-18T00:00"))); + assertThat(transaction1.getShares(), is(Values.Share.factorize(480))); + assertNull(transaction1.getSource()); + assertThat(transaction1.getNote(), is("GCM(CA38501D2041) SPLIT 1 FOR 25 (GCM, GRAN COLOMBIA GOLD CORP, CA38501D5010)")); + + assertThat(transaction1.getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + + // check delivery outbound (Auslieferung) transaction + transaction1 = (PortfolioTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(10).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction1.getType(), is(PortfolioTransaction.Type.DELIVERY_OUTBOUND)); + + assertThat(transaction1.getDateTime(), is(LocalDateTime.parse("2013-06-18T00:00"))); + assertThat(transaction1.getShares(), is(Values.Share.factorize(12000))); + assertNull(transaction1.getSource()); + assertThat(transaction1.getNote(), is("GCM(CA38501D2041) SPLIT 1 FOR 25 (GCM.OLD, GRAN COLOMBIA GOLD CORP, CA38501D2041)")); + + assertThat(transaction1.getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(transaction1.getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-04-01T09:34:06"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(5000))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 855937427 | Transaction-ID: 3452745495")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(1356.75)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(1350.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(6.75)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(0.27)))); + + // check 2nd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-04-01T09:34:13"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(6000))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 855937873 | Transaction-ID: 3452746284")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(1628.10)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(1620.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(8.10)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(0.27)))); + + // check 3rd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(2).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-04-01T09:34:14"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(1000))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 855938012 | Transaction-ID: 3452746368")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(271.35)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(270.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.35)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(0.27)))); + + // check 4th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(3).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:12:36"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813598116 | Transaction-ID: 3277427053")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 5th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(4).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:12:48"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(200))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813598306 | Transaction-ID: 3277427256")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(464.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(462.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(2.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 6th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(5).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:13:51"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813599636 | Transaction-ID: 3277432110")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 7th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(6).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:14:04"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813599932 | Transaction-ID: 3277432289")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 8th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(7).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:14:17"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813600188 | Transaction-ID: 3277432430")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 9th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(8).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:14:29"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813600464 | Transaction-ID: 3277432563")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 10th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(9).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:14:42"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813600662 | Transaction-ID: 3277435157")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 11th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(10).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:14:54"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813600885 | Transaction-ID: 3277435310")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(232.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(231.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(1.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.31)))); + + // check 12th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(11).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-03T09:32:48"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(1100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 813803887 | Transaction-ID: 3279084518")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(2574.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(2563.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(11.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.33)))); + + // check 13th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(12).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-02-19T15:36:15"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(10000))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 836265238 | Transaction-ID: 3370960179")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(27300.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(27200.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(100.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.72)))); + + // check 14th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(13).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2018-05-11T11:42:12"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(200))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 2107403097 | Transaction-ID: 9004794431")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(1390.90)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(1390.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.90)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(6.95)))); + + // check 15th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(14).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.SELL)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.SELL)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-10-25T00:00"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(12000))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("UUU.TEN2(CA91701P1TN3) MERGED(Voluntary Offer Allocation) FOR CAD 2.86000000 PER SHARE (UUU.TEN2, URANIUM ONE INC. - TENDER FOR CASH CAD, CA91701P1TN3)")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CAD", Values.Amount.factorize(34320.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CAD", Values.Amount.factorize(34320.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CAD", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CAD", Values.Quote.factorize(2.86)))); + } + + @Test + public void testiBFlexStatementFile02() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile02.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(8)); + assertThat(buySellTransactions.size(), is(9)); + assertThat(accountTransactions.size(), is(3)); + assertThat(results.size(), is(20)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("")); + assertThat(security1.getWkn(), is("272800")); + assertThat(security1.getTickerSymbol(), is("ORCL")); + assertThat(security1.getName(), is("ORACLE CORP")); + assertThat(security1.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security2 = results.stream().filter(SecurityItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security2.getIsin(), is("")); + assertThat(security2.getWkn(), is("268828466")); + assertThat(security2.getTickerSymbol(), is("ORCL170915P00050000")); + assertThat(security2.getName(), is("ORCL 15SEP17 50.0 P")); + assertThat(security2.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security2.getFeed(), is(YahooFinanceQuoteFeed.ID)); + + Security security3 = results.stream().filter(SecurityItem.class::isInstance).skip(2).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security3.getIsin(), is("")); + assertThat(security3.getWkn(), is("286599259")); + assertThat(security3.getTickerSymbol(), is("ORCL171117C00050000")); + assertThat(security3.getName(), is("ORCL 17NOV17 50.0 C")); + assertThat(security3.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security3.getFeed(), is(YahooFinanceQuoteFeed.ID)); + + Security security4 = results.stream().filter(SecurityItem.class::isInstance).skip(3).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security4.getIsin(), is("")); + assertThat(security4.getWkn(), is("311191362")); + assertThat(security4.getTickerSymbol(), is("PAYC181116C00120000")); + assertThat(security4.getName(), is("PAYC 16NOV18 120.0 C")); + assertThat(security4.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security4.getFeed(), is(YahooFinanceQuoteFeed.ID)); + + Security security5 = results.stream().filter(SecurityItem.class::isInstance).skip(4).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security5.getIsin(), is("DE0005190003")); + assertThat(security5.getWkn(), is("14094")); + assertThat(security5.getTickerSymbol(), is("BMW.DE")); + assertThat(security5.getName(), is("BAYERISCHE MOTOREN WERKE AG")); + assertThat(security5.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security5.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security6 = results.stream().filter(SecurityItem.class::isInstance).skip(5).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security6.getIsin(), is("DE0005140008")); + assertThat(security6.getWkn(), is("14121")); + assertThat(security6.getTickerSymbol(), is("DBK.DE")); + assertThat(security6.getName(), is("DEUTSCHE BANK AG-REGISTERED")); + assertThat(security6.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security6.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security7 = results.stream().filter(SecurityItem.class::isInstance).skip(6).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security7.getIsin(), is("DE000A0EQ578")); + assertThat(security7.getWkn(), is("43669257")); + assertThat(security7.getTickerSymbol(), is("H5E.DE")); + assertThat(security7.getName(), is("HELMA EIGENHEIMBAU AG")); + assertThat(security7.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security7.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security8 = results.stream().filter(SecurityItem.class::isInstance).skip(7).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security8.getIsin(), is("DE000BASF111")); + assertThat(security8.getWkn(), is("77680640")); + assertThat(security8.getTickerSymbol(), is("BAS")); + assertThat(security8.getName(), is("BASF SE")); + assertThat(security8.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security8.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-09-15T16:20"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1908991475")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4185.05)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4183.38)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.67)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(41.8338)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(5000.00)))); + + // check 2nd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.SELL)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.SELL)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-09-07T14:50:36"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1902533101")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(41.17)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(44.08)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2.91)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(0.4408)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(53.00)))); + + // check 3rd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(2).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-09-15T16:20"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1908991474")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(41.43)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(41.43)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(0.4143)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(49.50)))); + + // check 4th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(3).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.SELL)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.SELL)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-09-19T14:30:51"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1910911677")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(42.94)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(45.86)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2.92)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(0.4586)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(55.01)))); + + // check 5th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(4).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2018-05-11T11:45:45"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 2107408815 | Transaction-ID: 9004815263")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(578.32)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(577.78)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.54)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(5.7778)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(690.64)))); + + // check 6th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(5).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-11-06T03:35:13"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(141))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 970510813 | Transaction-ID: 3937447227")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(11573.95)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(11557.77)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(16.18)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(81.97)))); + + // check 7th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(6).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2016-08-05T04:34:09"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(10))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1622654675 | Transaction-ID: 6698911710")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(120.80)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(115.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5.80)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(11.50)))); + + // check 8th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(7).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-06-07T04:15:27"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(80))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1834141392 | Transaction-ID: 7684604106")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(3357.72)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(3351.92)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5.80)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(41.899)))); + + // check 9th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(8).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2015-12-08T03:42"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1452394455 | Transaction-ID: 5956040041")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(7188.05)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(7178.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(10.05)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(71.78)))); + + // check 1st dividends transaction + AccountTransaction transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-10-25T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(100))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("ORCL(US68389X1054) CASH DIVIDEND 0.19000000 USD PER SHARE (Ordinary Dividend)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(13.67)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(16.08)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2.41)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(19.00)))); + + // check transaction + // get transactions + Iterator iter = results.stream().filter(TransactionItem.class::isInstance).skip(1).iterator(); + assertThat(results.stream().filter(TransactionItem.class::isInstance).count(), is(3L)); + + Item item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-10-31T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(8.58)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("US SECURITIES SNAPSHOT AND FUTURES VALUE FOR AUG 2017")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2017-10-31T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(8.58)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("US SECURITIES SNAPSHOT AND FUTURES VALUE FOR JUL 2017")); + } + + @Test + public void testiBFlexStatementFile03() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile03.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(0)); + assertThat(buySellTransactions.size(), is(0)); + assertThat(accountTransactions.size(), is(1)); + assertThat(results.size(), is(1)); + new AssertImportActions().check(results, CurrencyUnit.USD); + + // check transaction + // get transactions + Iterator iter = results.stream().filter(TransactionItem.class::isInstance).iterator(); + assertThat(results.stream().filter(TransactionItem.class::isInstance).count(), is(1L)); + + Item item = iter.next(); + + // assert transaction + AccountTransaction transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.INTEREST)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2022-12-05T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.02)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("USD IBKR MANAGED SECURITIES (SYEP) INTEREST FOR NOV-2022")); + } + + @Test + public void testiBFlexStatementFile04() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile04.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, "CHF"); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("US0250727031")); + assertThat(security.getWkn(), is("385086964")); + assertThat(security.getTickerSymbol(), is("AVDE")); + assertThat(security.getName(), is("AVANTIS INTERNATIONAL EQUITY")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2022-01-25T10:59:10"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(14))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 1111111111")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of("CHF", Values.Amount.factorize(775.80)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of("CHF", Values.Amount.factorize(775.50)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of("CHF", Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of("CHF", Values.Amount.factorize(0.30)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of("CHF", Values.Quote.factorize(55.39285714)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(844.95)))); + } + + @Test + public void testiBFlexStatementFile05() throws IOException + { + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile05.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(2)); + assertThat(buySellTransactions.size(), is(2)); + assertThat(accountTransactions.size(), is(2)); + assertThat(results.size(), is(6)); + + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("US88579Y1010")); + assertThat(security1.getWkn(), is("13098504")); + assertThat(security1.getTickerSymbol(), is("MMM.DE")); + assertThat(security1.getName(), is("3M CO.")); + assertThat(security1.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security2 = results.stream().filter(SecurityItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security2.getIsin(), is("US12514G1085")); + assertThat(security2.getWkn(), is("130432552")); + assertThat(security2.getTickerSymbol(), is("CDW")); + assertThat(security2.getName(), is("CDW CORP/DE")); + assertThat(security2.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security2.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2018-02-09T11:19:29"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(7))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 2029054512 | Transaction-ID: 8626274484")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1275.25)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1269.45)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5.80)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(181.35)))); + + // check 2nd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2020-07-29T12:44:52"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(25))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 3004185992 | Transaction-ID: 13346288726")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(2870.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(2865.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(5.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.USD, Values.Quote.factorize(114.60)))); + + // check 1st dividends transaction + AccountTransaction transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2018-03-15T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(7))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("MMM(US88579Y1010) CASH DIVIDEND 1.36000000 USD PER SHARE (Ordinary Dividend)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.52)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.52)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + + // check 2nd dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(1).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-09-10T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(25))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("CDW(US12514G1085) CASH DIVIDEND USD 0.38 PER SHARE (Ordinary Dividend)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.50)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.50)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + } + @Test - public void testIBAcitvityStatement() throws IOException + public void testiBFlexStatementFile06() throws IOException { - InputStream activityStatement = getClass().getResourceAsStream("IBActivityStatement.xml"); - Client client = new Client(); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile06.xml"); Extractor.InputFile tempFile = createTempFile(activityStatement); List errors = new ArrayList<>(); + List results = extractor.extract(Collections.singletonList(tempFile), errors); - if (!errors.isEmpty()) - errors.forEach(Exception::printStackTrace); + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); - assertThat(errors.size(), is(0)); + assertThat(errors, empty()); + assertThat(securityItems.size(), is(2)); + assertThat(buySellTransactions.size(), is(2)); + assertThat(accountTransactions.size(), is(2)); + assertThat(results.size(), is(6)); + new AssertImportActions().check(results, CurrencyUnit.EUR); - results.stream().filter(i -> !(i instanceof SecurityItem)) - .forEach(i -> assertThat(i.getAmount(), notNullValue())); + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("US88579Y1010")); + assertThat(security1.getWkn(), is("13098504")); + assertThat(security1.getTickerSymbol(), is("MMM.DE")); + assertThat(security1.getName(), is("3M CO.")); + assertThat(security1.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); - List securityItems = results.stream().filter(i -> i instanceof SecurityItem).collect(Collectors.toList()); - List buySellTransactions = results.stream().filter(i -> i instanceof BuySellEntryItem) - .collect(Collectors.toList()); + Security security2 = results.stream().filter(SecurityItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security2.getIsin(), is("US12514G1085")); + assertThat(security2.getWkn(), is("130432552")); + assertThat(security2.getTickerSymbol(), is("CDW")); + assertThat(security2.getName(), is("CDW CORP/DE")); + assertThat(security2.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security2.getFeed(), is(AlphavantageQuoteFeed.ID)); - assertThat(securityItems.size(), is(5)); + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); - // 14 Trade item and one corporate transaction - assertThat(buySellTransactions.size(), is(15)); - assertThat(results.size(), is(31)); + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - assertFirstSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst()); - assertFirstTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).findFirst()); - - assertSecondSecurity(results.stream().filter(i -> i instanceof SecurityItem) - .reduce((previous, current) -> current).get()); - assertFourthTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).skip(3).findFirst()); - - assertOptionBuyTransaction(buySellTransactions.get(13)); - - assertInterestCharge(results.stream().filter(i -> i instanceof TransactionItem) - .filter(i -> i.getSubject() instanceof AccountTransaction - && ((AccountTransaction) i.getSubject()).getType() == Type.INTEREST_CHARGE) - .findFirst()); - assertTax(results.stream().filter(i -> i instanceof TransactionItem) - .filter(i -> i.getSubject() instanceof AccountTransaction - && ((AccountTransaction) i.getSubject()).getType() == Type.TAXES) - .findFirst()); - assertFee(results.stream().filter(i -> i instanceof TransactionItem) - .filter(i -> i.getSubject() instanceof AccountTransaction - && ((AccountTransaction) i.getSubject()).getType() == Type.FEES) - .skip(2).findFirst()); - assertFeeRefund(results.stream().filter(i -> i instanceof TransactionItem) - .filter(i -> i.getSubject() instanceof AccountTransaction - && ((AccountTransaction) i.getSubject()).getType() == Type.FEES_REFUND) - .findFirst()); - // TODO Check CorporateActions - } + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2018-02-09T11:19:29"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(7))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 2029054512 | Transaction-ID: 8626274484")); - private void assertTax(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1275.25)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1269.45)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5.80)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(181.35)))); - assertThat(entry.getType(), is(Type.TAXES)); - assertThat(entry.getMonetaryAmount(), is(Money.of("USD", 2_07L))); - assertThat(entry.getDateTime(), is(LocalDateTime.parse("2017-09-15T00:00"))); - } + // check 2nd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); - private void assertFee(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - assertThat(entry.getType(), is(Type.FEES)); - assertThat(entry.getMonetaryAmount(), is(Money.of("USD", 9_18L))); - assertThat(entry.getDateTime(), is(LocalDateTime.parse("2017-05-03T00:00"))); - } + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2020-07-29T12:44:52"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(25))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 3004185992 | Transaction-ID: 13346288726")); - private void assertFeeRefund(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2433.96)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2429.72)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4.24)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(97.1888)))); - assertThat(entry.getType(), is(Type.FEES_REFUND)); - assertThat(entry.getMonetaryAmount(), is(Money.of("USD", 9_18L))); - assertThat(entry.getDateTime(), is(LocalDateTime.parse("2017-05-03T00:00"))); - } + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(2870.00)))); - private void assertInterestCharge(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); + // check 1st dividends transaction + AccountTransaction transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2018-03-15T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(7))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("MMM(US88579Y1010) CASH DIVIDEND 1.36000000 USD PER SHARE (Ordinary Dividend)")); - assertThat(entry.getType(), is(Type.INTEREST_CHARGE)); - assertThat(entry.getMonetaryAmount(), is(Money.of("CAD", 15_17L))); - assertThat(entry.getDateTime(), is(LocalDateTime.parse("2013-02-05T00:00"))); + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(7.74)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(7.74)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + // check 2nd dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .skip(1).findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-09-10T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(25))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("CDW(US12514G1085) CASH DIVIDEND USD 0.38 PER SHARE (Ordinary Dividend)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(8.04)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(8.04)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9.50)))); } - private void assertFirstSecurity(Optional item) + @Test + public void testiBFlexStatementFile07() throws IOException { - assertThat(item.isPresent(), is(true)); - Security security = ((SecurityItem) item.get()).getSecurity(); - assertThat(security.getIsin(), is("CA38501D2041")); - assertThat(security.getWkn(), is("80845553")); - assertThat(security.getName(), is("GRAN COLOMBIA GOLD CORP")); - assertThat(security.getTickerSymbol(), is("GCM.TO")); - assertThat(security.getCurrencyCode(), is("CAD")); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile07.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("US8688612048")); + assertThat(security.getWkn(), is("27346611")); + assertThat(security.getTickerSymbol(), is("SGN.DE")); + assertThat(security.getName(), is("SURGUTNEFTEGAS-SP ADR")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-10-30T09:16:42"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(120))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 2592275115 | Transaction-ID: 11501005")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(726.00)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(720.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(6.00)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(6.00)))); } - private void assertSecondSecurity(Item item) + @Test + public void testiBFlexStatementFile08() throws IOException { - // Why is the second Security the GCM after Split ??? expected to be UUU - Security security = ((SecurityItem) item).getSecurity(); - assertThat(security.getIsin(), is("CA38501D5010")); - assertThat(security.getWkn(), is("129258970")); - assertThat(security.getName(), - is("GCM(CA38501D2041) SPLIT 1 FOR 25 (GCM, GRAN COLOMBIA GOLD CORP, CA38501D5010)")); - assertThat(security.getCurrencyCode(), is("CAD")); - - // setting GCM.TO as ticker symbol - // currently fails because the exchange is empty in corporate actions. + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile08.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(0)); + assertThat(accountTransactions.size(), is(2)); + assertThat(results.size(), is(3)); + new AssertImportActions().check(results, CurrencyUnit.USD); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("US8688612048")); + assertThat(security.getWkn(), is("27346611")); + assertThat(security.getTickerSymbol(), is("SGN")); + assertThat(security.getName(), is("SGN(US8688612048) CASH DIVIDEND USD 0.088051 PER SHARE - RU TAX")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check dividends transaction + AccountTransaction transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-08-18T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(120))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("SGN(US8688612048) CASH DIVIDEND USD 0.088051 PER SHARE (Ordinary Dividend)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(8.98)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(10.56)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(1.58)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(0.00)))); + + // check transaction + // get transactions + Iterator iter = results.stream().filter(TransactionItem.class::isInstance).skip(1).iterator(); + assertThat(results.stream().filter(TransactionItem.class::isInstance).count(), is(2L)); + + Item item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-08-18T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(3.67)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("SGN(US8688612048) CASH DIVIDEND USD 0.088051 PER SHARE - FEE")); } - private void assertFirstTransaction(Optional item) + @Test + public void testiBFlexStatementFile09() throws IOException { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.get().getSubject(); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile09.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(3)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("KY30744W1070")); + assertThat(security.getWkn(), is("517782617")); + assertThat(security.getTickerSymbol(), is("F1F")); + assertThat(security.getName(), is("FARFETCH LTD-CLASS A")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.EUR)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("GRAN COLOMBIA GOLD CORP")); - assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), is(Money.of("CAD", 1356_75L))); - assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-04-01T09:34"))); - assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(5000))); - assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), is(Money.of("CAD", 6_75L))); + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2022-12-02T06:26:11"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(450))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 464740365 | Transaction-ID: 1522126816")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2446.84)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2444.40)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2.44)))); assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), - is(Quote.of("CAD", Values.Quote.factorize(0.27)))); + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(5.432)))); + // check cancellation (Storno) transaction + NonImportableItem Cancelations = (NonImportableItem) results.stream() + .filter(NonImportableItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(Cancelations.getTypeInformation(), is(Messages.MsgErrorOrderCancellationUnsupported)); + assertNull(Cancelations.getSecurity()); + assertNull(Cancelations.getDate()); + assertNull(Cancelations.getSource()); + assertNull(Cancelations.getNote()); } - private void assertFourthTransaction(Optional item) + @Test + public void testiBFlexStatementFile10() throws IOException { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.get().getSubject(); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile10.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("US91680M1071")); + assertThat(security.getWkn(), is("460492620")); + assertThat(security.getTickerSymbol(), is("UPST")); + assertThat(security.getName(), is("UPSTART HOLDINGS INC")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("URANIUM ONE INC.")); - assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), is(Money.of("CAD", 232_00L))); - assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2013-01-02T15:12"))); - assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); - assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), is(Money.of("CAD", 1_00L))); + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2022-12-21T18:05:04"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(10))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Trade-ID: 116815359 | Transaction-ID: 415451625")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(132.81)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(132.50)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.31)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(13.25)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(140.83)))); } - private void assertOptionBuyTransaction(Item item) + @Test + public void testiBFlexStatementFile11() throws IOException { - assertThat(item.getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.getSubject(); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile11.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security.getIsin(), is("74347B243")); + assertThat(security.getWkn(), is("317467459")); + assertThat(security.getTickerSymbol(), is("QID")); + assertThat(security.getName(), is("PROSHARES ULTRASHORT QQQ")); + assertThat(security.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - assertThat(entry.getPortfolioTransaction().getSecurity().getTickerSymbol(), is("FB180921C00200000")); + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-12-13T10:36:40"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(14))); + assertNull(entry.getSource()); + assertNull(entry.getNote()); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(344.57)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(344.40)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.17)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(24.60)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(350.18)))); } @Test - public void testThatExceptionIsAddedForNonFlexStatementDocuments() throws IOException + public void testiBFlexStatementFile12() throws IOException { - InputStream otherFile = getClass().getResourceAsStream("../pdf/comdirect/Dividende05.txt"); - Extractor.InputFile tempFile = createTempFile(otherFile); - Client client = new Client(); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); + + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile12.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + List errors = new ArrayList<>(); + List results = extractor.extract(Collections.singletonList(tempFile), errors); - assertThat(results.isEmpty(), is(true)); - assertThat(errors.size(), is(1)); + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(2)); + assertThat(buySellTransactions.size(), is(4)); + assertThat(accountTransactions.size(), is(12)); + assertThat(results.size(), is(18)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("XXXXXCAD")); + assertThat(security1.getWkn(), is("XXXXXCAD")); + assertThat(security1.getTickerSymbol(), is("XXXXXCAD.TO")); + assertThat(security1.getName(), is("XXXXXCAD")); + assertThat(security1.getCurrencyCode(), is("CAD")); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); + + Security security2 = results.stream().filter(SecurityItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security2.getIsin(), is("XXXXXUSD")); + assertThat(security2.getWkn(), is("XXXXXUSD")); + assertThat(security2.getTickerSymbol(), is("XXXXXUSD")); + assertThat(security2.getName(), is("XXXXXUSD")); + assertThat(security2.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security2.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check 1st buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-01-15T15:32:01"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(400))); + assertNull(entry.getSource()); + assertNull(entry.getNote()); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(848.65)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(845.90)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(2.75)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(2.11475)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of("CAD", Values.Amount.factorize(1234.00)))); + + // check 2nd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(1).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-01-02T09:30:01"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(200))); + assertNull(entry.getSource()); + assertNull(entry.getNote()); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5203.94)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(5202.15)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.79)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(26.01075)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(5814.00)))); + + // check 3rd buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(2).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-01-08T15:59:16"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(35))); + assertNull(entry.getSource()); + assertNull(entry.getNote()); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4093.22)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4091.42)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.80)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(116.8977143)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(4545.50)))); + + // check 4th buy sell transaction + entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).skip(3).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2019-01-15T14:17:05"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(50))); + assertNull(entry.getSource()); + assertNull(entry.getNote()); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1349.34)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1347.55)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.79)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(26.951)))); + + grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(1504.50)))); + + // check transaction + // get transactions + Iterator iter = results.stream().filter(TransactionItem.class::isInstance).iterator(); + assertThat(results.stream().filter(TransactionItem.class::isInstance).count(), is(12L)); + + Item item = iter.next(); + + // assert transaction + AccountTransaction transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.TAX_REFUND)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2019-01-31T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.81)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.TAXES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2019-01-31T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.49)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.TAXES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-02T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(7.68)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.TAXES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-02T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(3.55)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + // check 1st dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance).skip(4) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2019-12-31T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(0))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXCAD)")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(26.95)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(31.71)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4.76)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of("CAD", Values.Amount.factorize(46.00)))); + + // check 2nd dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance).skip(5) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-15T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(0))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXCAD")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(23.24)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(29.44)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(6.20)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of("CAD", Values.Amount.factorize(42.75)))); + + // check 3rd dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance).skip(6) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-02T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(0))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(51.19)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(51.19)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(57.19)))); + + // check 4th dividends transaction + transaction = (AccountTransaction) results.stream().filter(TransactionItem.class::isInstance).skip(7) + .findFirst().orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(transaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); + + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-02T00:00"))); + assertThat(transaction.getShares(), is(Values.Share.factorize(0))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + assertThat(transaction.getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(23.72)))); + assertThat(transaction.getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(23.72)))); + assertThat(transaction.getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(transaction.getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + + grossValueUnit = transaction.getUnit(Unit.Type.GROSS_VALUE).orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(26.50)))); + + // check transaction + // get transactions + iter = results.stream().filter(TransactionItem.class::isInstance).skip(8).iterator(); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.34)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.FEES)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-03T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1.34)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("XXXXXUSD")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.DEPOSIT)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-20T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(999.00)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("CASH RECEIPTS / ELECTRONIC FUND TRANSFERS")); + + item = iter.next(); + + // assert transaction + transaction = (AccountTransaction) item.getSubject(); + assertThat(transaction.getType(), is(AccountTransaction.Type.INTEREST_CHARGE)); + assertThat(transaction.getDateTime(), is(LocalDateTime.parse("2020-01-06T00:00"))); + assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(8.92)))); + assertNull(transaction.getSource()); + assertThat(transaction.getNote(), is("USD DEBIT INT FOR DEC-2019")); } - private Extractor.InputFile createTempFile(InputStream input) throws IOException + @Test + public void testiBFlexStatementFile13() throws IOException { - File tempFile = File.createTempFile("iBFlexStatementExtractorTest", null); - FileOutputStream fos = new FileOutputStream(tempFile); + IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(new Client()); - IOUtils.copy(input, fos); - return new Extractor.InputFile(tempFile); + InputStream activityStatement = getClass().getResourceAsStream("testiBFlexStatementFile13.xml"); + Extractor.InputFile tempFile = createTempFile(activityStatement); + + List errors = new ArrayList<>(); + + List results = extractor.extract(Collections.singletonList(tempFile), errors); + + List securityItems = results.stream().filter(SecurityItem.class::isInstance) // + .collect(Collectors.toList()); + List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) // + .collect(Collectors.toList()); + List accountTransactions = results.stream().filter(TransactionItem.class::isInstance) // + .collect(Collectors.toList()); + + assertThat(errors, empty()); + assertThat(securityItems.size(), is(1)); + assertThat(buySellTransactions.size(), is(1)); + assertThat(accountTransactions.size(), is(0)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + Security security1 = results.stream().filter(SecurityItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSecurity(); + assertThat(security1.getIsin(), is("US6706991071")); + assertThat(security1.getWkn(), is("290662729")); + assertThat(security1.getTickerSymbol(), is("QQQX")); + assertThat(security1.getName(), is("USD QQQX")); + assertThat(security1.getCurrencyCode(), is(CurrencyUnit.USD)); + assertThat(security1.getFeed(), is(AlphavantageQuoteFeed.ID)); + + // check buy sell transaction + BuySellEntry entry = (BuySellEntry) results.stream().filter(BuySellEntryItem.class::isInstance).findFirst() + .orElseThrow(IllegalArgumentException::new).getSubject(); + + assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); + assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); + + assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2020-09-09T11:22:33"))); + assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(50))); + assertNull(entry.getSource()); + assertThat(entry.getNote(), is("Transaction-ID: N/A")); + + assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1063.29)))); + assertThat(entry.getPortfolioTransaction().getGrossValue(), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(1059.05)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(0.00)))); + assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), + is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4.24)))); + assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), + is(Quote.of(CurrencyUnit.EUR, Values.Quote.factorize(21.181)))); + + Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) + .orElseThrow(IllegalArgumentException::new); + assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(1250.00)))); } } diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithAccountDetailsTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithAccountDetailsTest.java deleted file mode 100644 index 9947adac93..0000000000 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithAccountDetailsTest.java +++ /dev/null @@ -1,189 +0,0 @@ -package name.abuchen.portfolio.datatransfer.ibflex; - -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 static org.junit.Assert.assertTrue; - -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; -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.Money; -import name.abuchen.portfolio.money.Quote; -import name.abuchen.portfolio.money.Values; -import name.abuchen.portfolio.online.impl.AlphavantageQuoteFeed; -import name.abuchen.portfolio.online.impl.YahooFinanceQuoteFeed; - -@SuppressWarnings("nls") -public class IBFlexStatementExtractorWithAccountDetailsTest -{ - - private List runExtractor(List errors) throws IOException - { - InputStream activityStatement = getClass().getResourceAsStream("IBActivityStatementWithAccountDetails.xml"); - Client client = new Client(); - Extractor.InputFile tempFile = createTempFile(activityStatement); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); - - return extractor.extract(Collections.singletonList(tempFile), errors); - } - - @Test - public void testIBAcitvityStatement() throws IOException - { - List errors = new ArrayList(); - List results = runExtractor(errors); - assertTrue(errors.isEmpty()); - int numSecurity = 8; - int numBuySell = 9; - int numTransactions = 4; - - results.stream().filter(i -> !(i instanceof SecurityItem)) - .forEach(i -> assertThat(i.getAmount(), notNullValue())); - - List securityItems = results.stream().filter(i -> i instanceof SecurityItem) - .collect(Collectors.toList()); - - assertThat(securityItems.size(), is(numSecurity)); - - assertOptionSecurity((SecurityItem) securityItems.get(2)); - - List buySellTransactions = results.stream().filter(i -> i instanceof BuySellEntryItem) - .collect(Collectors.toList()); - - assertThat(buySellTransactions.size(), is(numBuySell)); - assertOptionBuySellTransaction((BuySellEntryItem) buySellTransactions.get(2)); - - List accountTransactions = results.stream().filter(i -> i instanceof TransactionItem) - .collect(Collectors.toList()); - - assertThat(accountTransactions.size(), is(numTransactions)); - - assertThat(results.size(), is(numSecurity + numBuySell + numTransactions)); - - assertSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst()); - assertFirstTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).findFirst()); - } - - @Test - public void testSymbolTranslation() throws IOException - { - List errors = new ArrayList(); - List results = runExtractor(errors); - List securityItems = results.stream().filter(i -> i instanceof SecurityItem) - .collect(Collectors.toList()); - - assertThat(securityItems.get(0).getSecurity().getTickerSymbol(), is("ORCL")); - assertThat(securityItems.get(0).getSecurity().getFeed(), is(AlphavantageQuoteFeed.ID)); - assertThat(securityItems.get(3).getSecurity().getTickerSymbol(), is("PAYC181116C00120000")); - assertThat(securityItems.get(3).getSecurity().getFeed(), is(YahooFinanceQuoteFeed.ID)); - assertThat(securityItems.get(4).getSecurity().getTickerSymbol(), is("BMW.DE")); - assertThat(securityItems.get(5).getSecurity().getFeed(), is(AlphavantageQuoteFeed.ID)); - assertThat(securityItems.get(5).getSecurity().getTickerSymbol(), is("DBK.DE")); - assertThat(securityItems.get(6).getSecurity().getTickerSymbol(), is("H5E.DE")); - assertThat(securityItems.get(6).getSecurity().getFeed(), is(AlphavantageQuoteFeed.ID)); - assertThat(securityItems.get(7).getSecurity().getTickerSymbol(), is("BAS")); - assertThat(securityItems.get(7).getSecurity().getFeed(), is(AlphavantageQuoteFeed.ID)); - - } - - private void assertOptionSecurity(SecurityItem item) - { - assertThat(item.getSecurity().getFeed(), is(YahooFinanceQuoteFeed.ID)); - assertThat(item.getSecurity().getTickerSymbol(), is("ORCL171117C00050000")); - - } - - private void assertOptionBuySellTransaction(BuySellEntryItem item) - { - assertThat(item.getShares(), is((long) 100 * Values.Share.factor())); - } - // private void assertInterestCharge(Optional item) - // { - // assertThat(item.isPresent(), is(true)); - // assertThat(item.get().getSubject(), - // instanceOf(AccountTransaction.class)); - // AccountTransaction entry = (AccountTransaction) item.get().getSubject(); - // - // assertThat(entry.getType(), is(Type.INTEREST_CHARGE)); - // assertThat(entry.getMonetaryAmount(), is(Money.of("CAD", 15_17L))); - // assertThat(entry.getDate(), is(LocalDate.parse("2013-02-05"))); - // } - - private void assertSecurity(Optional item) - { - assertThat(item.isPresent(), is(true)); - Security security = ((SecurityItem) item.get()).getSecurity(); - assertThat(security.getWkn(), is("272800")); - assertThat(security.getName(), is("ORACLE CORP")); - assertThat(security.getTickerSymbol(), is("ORCL")); - assertThat(security.getCurrencyCode(), is("USD")); - } - - private void assertFirstTransaction(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.get().getSubject(); - - assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); - assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - - assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("ORACLE CORP")); - assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), is(Money.of("EUR", 4185_05L))); - assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2017-09-15T16:20"))); - assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(100))); - assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), is(Money.of("EUR", 1_67L))); - // 100 shares at 50 USD minus 2USD transaction cost is 49.98 USD per - // share times 0.83701 is 41.8338 - assertThat(entry.getPortfolioTransaction().getGrossPricePerShare(), - is(Quote.of("EUR", Values.Quote.factorize(41.8338)))); - - } - - @Test - public void testThatExceptionIsAddedForNonFlexStatementDocuments() throws IOException - { - InputStream otherFile = getClass().getResourceAsStream("../pdf/comdirect/Dividende05.txt"); - Extractor.InputFile tempFile = createTempFile(otherFile); - Client client = new Client(); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); - List errors = new ArrayList(); - List results = extractor.extract(Collections.singletonList(tempFile), errors); - - assertThat(results.isEmpty(), is(true)); - assertThat(errors.size(), is(1)); - } - - 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); - } -} diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithDuplicateTransactionsTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithDuplicateTransactionsTest.java deleted file mode 100644 index d8074ba8e5..0000000000 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithDuplicateTransactionsTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package name.abuchen.portfolio.datatransfer.ibflex; - -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 static org.junit.Assert.assertTrue; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -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; -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.Client; -import name.abuchen.portfolio.model.Transaction.Unit; -import name.abuchen.portfolio.money.CurrencyUnit; -import name.abuchen.portfolio.money.Money; - -@SuppressWarnings("nls") -public class IBFlexStatementExtractorWithDuplicateTransactionsTest -{ - private List runExtractor(List errors) throws IOException - { - InputStream activityStatement = getClass().getResourceAsStream("IBActivityStatementWithDuplicateTransactions.xml"); - Client client = new Client(); - Extractor.InputFile tempFile = createTempFile(activityStatement); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); - - return extractor.extract(Collections.singletonList(tempFile), errors); - } - - @Test - public void testIBAcitvityStatement() throws IOException - { - List errors = new ArrayList(); - List results = runExtractor(errors); - assertTrue(errors.isEmpty()); - int numSecurity = 0; - int numBuySell = 0; - int numTransactions = 1; - - results.stream().filter(i -> !(i instanceof SecurityItem)) - .forEach(i -> assertThat(i.getAmount(), notNullValue())); - - List securityItems = results.stream().filter(i -> i instanceof SecurityItem) - .collect(Collectors.toList()); - - assertThat(securityItems.size(), is(numSecurity)); - - List buySellTransactions = results.stream().filter(i -> i instanceof BuySellEntryItem) - .collect(Collectors.toList()); - - assertThat(buySellTransactions.size(), is(numBuySell)); - - List accountTransactions = results.stream().filter(i -> i instanceof TransactionItem) - .collect(Collectors.toList()); - - assertThat(accountTransactions.size(), is(numTransactions)); - - assertThat(results.size(), is(numSecurity + numBuySell + numTransactions)); - - assertFirstSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst()); - assertFirstTransaction(results.stream().filter(i -> i instanceof TransactionItem).findFirst()); - assertSecondTransaction(results.stream().filter(i -> i instanceof TransactionItem).skip(1).findFirst()); - } - - private void assertFirstTransaction(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction transaction = (AccountTransaction) item.get().getSubject(); - - assertThat(transaction.getType(), is(AccountTransaction.Type.INTEREST)); - - assertThat(transaction.getMonetaryAmount(), is(Money.of(CurrencyUnit.USD, 0_02L))); - assertThat(transaction.getCurrencyCode(), is(CurrencyUnit.USD)); - assertTrue(!transaction.getUnit(Unit.Type.GROSS_VALUE).isPresent()); - } - - private void assertSecondTransaction(Optional item) - { - assertThat(item.isPresent(), is(false)); - } - - private void assertFirstSecurity(Optional item) - { - assertThat(item.isPresent(), is(false)); - } - - 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); - } -} diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithFixGrossValueBuySellTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithFixGrossValueBuySellTest.java deleted file mode 100644 index b67dcd8005..0000000000 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithFixGrossValueBuySellTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package name.abuchen.portfolio.datatransfer.ibflex; - -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 static org.junit.Assert.assertNull; - -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; -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.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.Values; - -@SuppressWarnings("nls") -public class IBFlexStatementExtractorWithFixGrossValueBuySellTest -{ - @Test - public void testIBAcitvityStatement() throws IOException - { - InputStream activityStatement = getClass().getResourceAsStream("IBActivityStatementWithFixGrossValueBuySell.xml"); - Client client = new Client(); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); - - Extractor.InputFile tempFile = createTempFile(activityStatement); - - List errors = new ArrayList<>(); - List results = extractor.extract(Collections.singletonList(tempFile), errors); - - if (!errors.isEmpty()) - errors.forEach(Exception::printStackTrace); - - assertThat(errors.size(), is(0)); - - results.stream().filter(i -> !(i instanceof SecurityItem)) - .forEach(i -> assertThat(i.getAmount(), notNullValue())); - - List securityItems = results.stream().filter(i -> i instanceof SecurityItem).collect(Collectors.toList()); - List buySellTransactions = results.stream().filter(i -> i instanceof BuySellEntryItem) - .collect(Collectors.toList()); - - assertThat(securityItems.size(), is(1)); - - // 1 Trade item and one corporate transaction - assertThat(buySellTransactions.size(), is(1)); - assertThat(results.size(), is(2)); - - assertFirstSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst()); - assertFirstTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).findFirst()); - } - - private void assertFirstSecurity(Optional item) - { - assertThat(item.isPresent(), is(true)); - Security security = ((SecurityItem) item.get()).getSecurity(); - assertThat(security.getIsin(), is("US0250727031")); - assertThat(security.getWkn(), is("385086964")); - assertThat(security.getName(), is("AVANTIS INTERNATIONAL EQUITY")); - assertThat(security.getTickerSymbol(), is("AVDE")); - assertThat(security.getCurrencyCode(), is("USD")); - } - - private void assertFirstTransaction(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.get().getSubject(); - - assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY)); - assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY)); - - assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), is(Money.of("CHF", 775_79L))); - assertThat(entry.getPortfolioTransaction().getDateTime(), is(LocalDateTime.parse("2022-01-25T10:59"))); - assertThat(entry.getPortfolioTransaction().getShares(), is(Values.Share.factorize(14))); - assertNull(entry.getSource()); - assertThat(entry.getNote(), is("AVANTIS INTERNATIONAL EQUITY")); - - assertThat(entry.getPortfolioTransaction().getMonetaryAmount(), - is(Money.of("CHF", Values.Amount.factorize(775.79)))); - assertThat(entry.getPortfolioTransaction().getGrossValue(), - is(Money.of("CHF", Values.Amount.factorize(775.49)))); - assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.TAX), - is(Money.of("CHF", Values.Amount.factorize(0.00)))); - assertThat(entry.getPortfolioTransaction().getUnitSum(Unit.Type.FEE), - is(Money.of("CHF", Values.Amount.factorize(0.30)))); - - Unit grossValueUnit = entry.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE) - .orElseThrow(IllegalArgumentException::new); - assertThat(grossValueUnit.getForex(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(844.95)))); - } - - 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); - } -} diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendNoAccountInfoTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendNoAccountInfoTest.java deleted file mode 100644 index 57ae8553d9..0000000000 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendNoAccountInfoTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package name.abuchen.portfolio.datatransfer.ibflex; - -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; -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 runExtractor(List 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 errors = new ArrayList<>(); - List 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 securityItems = results.stream().filter(SecurityItem.class::isInstance) - .collect(Collectors.toList()); - - assertThat(securityItems.size(), is(numSecurity)); - - List buySellTransactions = results.stream().filter(BuySellEntryItem.class::isInstance) - .collect(Collectors.toList()); - - assertThat(buySellTransactions.size(), is(numBuySell)); - - List 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) - { - 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) - { - 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) - { - 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); - } -} diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendTest.java deleted file mode 100644 index 8103abeaeb..0000000000 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractorWithForeignDividendTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package name.abuchen.portfolio.datatransfer.ibflex; - -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 static org.junit.Assert.assertTrue; - -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; -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.Money; -import name.abuchen.portfolio.money.Quote; -import name.abuchen.portfolio.money.Values; - -@SuppressWarnings("nls") -public class IBFlexStatementExtractorWithForeignDividendTest -{ - - private List runExtractor(List errors) throws IOException - { - InputStream activityStatement = getClass().getResourceAsStream("IBActivityStatementWithForeignDividend.xml"); - Client client = new Client(); - Extractor.InputFile tempFile = createTempFile(activityStatement); - IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client); - - return extractor.extract(Collections.singletonList(tempFile), errors); - } - - @Test - public void testIBAcitvityStatement() throws IOException - { - List errors = new ArrayList(); - List results = runExtractor(errors); - assertTrue(errors.isEmpty()); - int numSecurity = 2; - int numBuySell = 2; - int numTransactions = 2; - - results.stream().filter(i -> !(i instanceof SecurityItem)) - .forEach(i -> assertThat(i.getAmount(), notNullValue())); - - List securityItems = results.stream().filter(i -> i instanceof SecurityItem) - .collect(Collectors.toList()); - - assertThat(securityItems.size(), is(numSecurity)); - - List buySellTransactions = results.stream().filter(i -> i instanceof BuySellEntryItem) - .collect(Collectors.toList()); - - assertThat(buySellTransactions.size(), is(numBuySell)); - - List accountTransactions = results.stream().filter(i -> i instanceof TransactionItem) - .collect(Collectors.toList()); - - assertThat(accountTransactions.size(), is(numTransactions)); - - assertThat(results.size(), is(numSecurity + numBuySell + numTransactions)); - - assertFirstSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst()); - assertFirstBuySell(results.stream().filter(i -> i instanceof BuySellEntryItem).findFirst()); - assertFirstTransaction(results.stream().filter(i -> i instanceof TransactionItem).findFirst()); - assertSecondTransaction(results.stream().filter(i -> i instanceof TransactionItem).skip(1).findFirst()); - } - - private void assertFirstSecurity(Optional item) - { - assertThat(item.isPresent(), is(true)); - Security security = ((SecurityItem) item.get()).getSecurity(); - assertThat(security.getIsin(), is("US88579Y1010")); - assertThat(security.getWkn(), is("13098504")); - assertThat(security.getName(), is("3M CO.")); - assertThat(security.getTickerSymbol(), is("MMM.DE")); - assertThat(security.getCurrencyCode(), is("EUR")); - } - - private void assertFirstBuySell(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class)); - BuySellEntry entry = (BuySellEntry) item.get().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.")); - 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) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); - - assertThat(entry.getType(), is(AccountTransaction.Type.DIVIDENDS)); - - assertThat(entry.getSecurity().getName(), is("3M CO.")); - assertThat(entry.getSecurity().getIsin(), is("US88579Y1010")); - assertThat(entry.getMonetaryAmount(), is(Money.of("EUR", 7_74L))); - assertThat(entry.getCurrencyCode(), is("EUR")); - assertThat(entry.getSecurity().getCurrencyCode(), is("EUR")); - assertTrue(!entry.getUnit(Unit.Type.GROSS_VALUE).isPresent()); - } - - private void assertSecondTransaction(Optional item) - { - assertThat(item.isPresent(), is(true)); - assertThat(item.get().getSubject(), instanceOf(AccountTransaction.class)); - AccountTransaction entry = (AccountTransaction) item.get().getSubject(); - - assertThat(entry.getType(), is(AccountTransaction.Type.DIVIDENDS)); - - assertThat(entry.getSecurity().getName(), is("CDW CORP/DE")); - assertThat(entry.getSecurity().getIsin(), is("US12514G1085")); - assertThat(entry.getMonetaryAmount(), is(Money.of("EUR", 8_04L))); - assertThat(entry.getCurrencyCode(), is("EUR")); - assertThat(entry.getSecurity().getCurrencyCode(), is("USD")); - assertTrue(entry.getUnit(Unit.Type.GROSS_VALUE).isPresent()); - } - - 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); - } -} diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatement.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile01.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatement.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile01.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithAccountDetails.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile02.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithAccountDetails.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile02.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithDuplicateTransactions.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile03.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithDuplicateTransactions.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile03.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithFixGrossValueBuySell.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile04.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithFixGrossValueBuySell.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile04.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithForeignDividendNoAccountInfo.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile05.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithForeignDividendNoAccountInfo.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile05.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithForeignDividend.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile06.xml similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/IBActivityStatementWithForeignDividend.xml rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile06.xml diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile07.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile07.xml new file mode 100644 index 0000000000..4db7754edf --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile07.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile08.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile08.xml new file mode 100644 index 0000000000..495dc1ef44 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile08.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile09.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile09.xml new file mode 100644 index 0000000000..e35924fe4d --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile09.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile10.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile10.xml new file mode 100644 index 0000000000..3f70873233 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile10.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile11.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile11.xml new file mode 100644 index 0000000000..b89e332eb0 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile11.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile12.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile12.xml new file mode 100644 index 0000000000..9a747a662e --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile12.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile13.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile13.xml new file mode 100644 index 0000000000..ca5c89d7ff --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/ibflex/testiBFlexStatementFile13.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java index e70de6d396..bc8c7c5844 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java @@ -40,7 +40,8 @@ public class ExtractorUtils private static final DateTimeFormatter[] DATE_FORMATTER_US = { // DateTimeFormatter.ofPattern("dd LLL yyyy", Locale.US), //$NON-NLS-1$ - DateTimeFormatter.ofPattern("d LLL yyyy", Locale.US) }; //$NON-NLS-1$ + DateTimeFormatter.ofPattern("d LLL yyyy", Locale.US), //$NON-NLS-1$ + DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US) }; //$NON-NLS-1$ private static final DateTimeFormatter[] DATE_FORMATTER_CANADA = { // DateTimeFormatter.ofPattern("dd LLL yyyy", Locale.CANADA) }; //$NON-NLS-1$ @@ -69,6 +70,7 @@ public class ExtractorUtils DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss", Locale.GERMANY), //$NON-NLS-1$ DateTimeFormatter.ofPattern("dd.MM.yyyy HH.mm.ss", Locale.GERMANY), //$NON-NLS-1$ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US), //$NON-NLS-1$ + DateTimeFormatter.ofPattern("yyyyMMdd HHmmss", Locale.US), //$NON-NLS-1$ DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", Locale.UK), //$NON-NLS-1$ DateTimeFormatter.ofPattern("dd.MM.yyyy hh:mm:ss a", Locale.UK) }; //$NON-NLS-1$ diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java index af990bb6a3..4a55606cff 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ibflex/IBFlexStatementExtractor.java @@ -5,20 +5,19 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; @@ -52,11 +51,28 @@ @SuppressWarnings("nls") public class IBFlexStatementExtractor implements Extractor { + /** + * https://ibkrguides.com/reportingreference/reportguide/tradesfq.htm + */ + + private static final String TO_BE_DELETED = "to_be_deleted"; //$NON-NLS-1$ + private final Client client; private List allSecurities; private Map exchanges; + public Client getClient() + { + return client; + } + + @Override + public String getLabel() + { + return Messages.IBXML_Label; + } + public IBFlexStatementExtractor(Client client) { this.client = client; @@ -74,29 +90,17 @@ public IBFlexStatementExtractor(Client client) this.exchanges.put("TGATE", "DE"); this.exchanges.put("SWB", "SG"); this.exchanges.put("FWB", "F"); - } - - public Client getClient() - { - return client; - } - - private LocalDateTime convertDate(String date) throws DateTimeParseException - { - if (date.length() > 8) - { - return LocalDate.parse(date).atStartOfDay(); - } - else - { - return LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd")).atStartOfDay(); - } - } - - private LocalDateTime convertDate(String date, String time) throws DateTimeParseException - { - return LocalDateTime.parse(String.format("%s %s", date, time), DateTimeFormatter.ofPattern("yyyyMMdd HHmmss")) - .withSecond(0).withNano(0); + // CFD + this.exchanges.put("IBUS500", "^GSPC"); + this.exchanges.put("IBUS30", "^DJI"); + this.exchanges.put("IBUST100", "^IXIC"); + this.exchanges.put("IBGB100", "^FTSE"); + this.exchanges.put("IBEU50", "^STOXX50E"); + this.exchanges.put("IBDE30", "^GDAXI"); + this.exchanges.put("IBFR40", "^FCHI"); + this.exchanges.put("IBNL25", "^AEX"); + this.exchanges.put("IBJP225", "^N225"); + this.exchanges.put("IBAU200", "^AXJO"); } /** @@ -104,7 +108,7 @@ private LocalDateTime convertDate(String date, String time) throws DateTimeParse * currently only imports Trades, Corporate Transactions and Cash * Transactions. */ - /* package */IBFlexStatementExtractorResult importActivityStatement(InputStream f) + /* package */ IBFlexStatementExtractorResult importActivityStatement(InputStream f) { IBFlexStatementExtractorResult result = new IBFlexStatementExtractorResult(); try @@ -126,61 +130,25 @@ private LocalDateTime convertDate(String date, String time) throws DateTimeParse return result; } - /** - * Return currency as valid currency code (in the sense that PP is - * supporting this currency code) - */ - private String asCurrencyUnit(String currency) - { - if (currency == null) - return CurrencyUnit.EUR; - - CurrencyUnit unit = CurrencyUnit.getInstance(currency.trim()); - return unit == null ? CurrencyUnit.EUR : unit.getCurrencyCode(); - } - - @Override - public String getLabel() - { - return Messages.IBXML_Label; - } - - @Override - public List extract(SecurityCache securityCache, Extractor.InputFile inputFile, List errors) - { - try (FileInputStream in = new FileInputStream(inputFile.getFile())) - { - IBFlexStatementExtractorResult result = importActivityStatement(in); - errors.addAll(result.getErrors()); - return result.getResults(); - } - catch (IOException e) - { - errors.add(e); - return Collections.emptyList(); - } - } - private class IBFlexStatementExtractorResult { private static final String ASSETKEY_STOCK = "STK"; private static final String ASSETKEY_OPTION = "OPT"; private static final String ASSETKEY_FUTURE_OPTION = "FOP"; - + private static final String ASSETKEY_CFD = "CFD"; + private Document document; private List errors = new ArrayList<>(); private List results = new ArrayList<>(); private String ibAccountCurrency = null; private Function importAccountInformation = element -> { - String currency = asCurrencyUnit(element.getAttribute("currency")); + String currency = asCurrencyCode(element.getAttribute("currency")); if (currency != null && !currency.isEmpty()) { CurrencyUnit currencyUnit = CurrencyUnit.getInstance(currency); if (currencyUnit != null) - { ibAccountCurrency = currency; - } } return null; }; @@ -197,41 +165,30 @@ private class IBFlexStatementExtractorResult if (element.hasAttribute("reportDate")) { if (element.getAttribute("reportDate").length() == 15) - { - transaction.setDateTime(convertDate(element.getAttribute("reportDate").substring(0, 8))); - } + transaction.setDateTime(ExtractorUtils.asDate(element.getAttribute("reportDate").substring(0, 8))); else - { - transaction.setDateTime(convertDate(element.getAttribute("reportDate"))); - } + transaction.setDateTime(ExtractorUtils.asDate(element.getAttribute("reportDate"))); } else { if (element.getAttribute("dateTime").length() == 15) - { - transaction.setDateTime(convertDate(element.getAttribute("dateTime").substring(0, 8))); - } + transaction.setDateTime(ExtractorUtils.asDate(element.getAttribute("dateTime").substring(0, 8))); else - { - transaction.setDateTime(convertDate(element.getAttribute("dateTime"))); - } + transaction.setDateTime(ExtractorUtils.asDate(element.getAttribute("dateTime"))); } - Double amount = Double.parseDouble(element.getAttribute("amount")); - String currency = asCurrencyUnit(element.getAttribute("currency")); + Money amount = Money.of(asCurrencyCode(element.getAttribute("currency")), + asAmount(element.getAttribute("amount"))); + Double isNegativAmount = Double.parseDouble(element.getAttribute("amount")); // Set Transaction Type String type = element.getAttribute("type"); if (type.equals("Deposits") || type.equals("Deposits & Withdrawals") || type.equals("Deposits/Withdrawals")) { - if (amount >= 0) - { + if (isNegativAmount >= 0) transaction.setType(AccountTransaction.Type.DEPOSIT); - } else - { transaction.setType(AccountTransaction.Type.REMOVAL); - } } else if (type.equals("Dividends") || type.equals("Payment In Lieu Of Dividends")) { @@ -249,15 +206,11 @@ else if (type.equals("Withholding Tax")) if (element.getAttribute("symbol").length() > 0) transaction.setSecurity(this.getOrCreateSecurity(element, true)); - if (amount <= 0) - { + // Positive taxes are a tax refund: see #310 + if (isNegativAmount <= 0) transaction.setType(AccountTransaction.Type.TAXES); - } else - { - // Positive taxes are a tax refund: see #310 transaction.setType(AccountTransaction.Type.TAX_REFUND); - } } else if (type.equals("Broker Interest Received")) { @@ -270,15 +223,11 @@ else if (type.equals("Broker Interest Paid")) } else if (type.equals("Other Fees")) { - if (amount <= 0) - { + // Positive values are a fee refund + if (isNegativAmount <= 0) transaction.setType(AccountTransaction.Type.FEES); - } else - { - // Positive values are a fee refund transaction.setType(AccountTransaction.Type.FEES_REFUND); - } } else { @@ -291,41 +240,54 @@ else if (type.equals("Other Fees")) // if the account currency differs from transaction currency // convert currency, if there is a matching security with the // account currency - if (this.ibAccountCurrency != null && !this.ibAccountCurrency.equals(currency)) // NOSONAR + if (this.ibAccountCurrency != null && !this.ibAccountCurrency.equals(amount.getCurrencyCode())) // NOSONAR { - // matching isin & base currency + // matching isin or wkn & base currency boolean foundIsinBase = false; // matching isin & transaction currency boolean foundIsinTransaction = false; + // matching conid (wkn) & transaction currency + boolean foundWknTransaction = false; for (Security s : allSecurities) { String isin = element.getAttribute("isin"); - // Find security with same isin & currency + String conid = element.getAttribute("conid"); + + // Find security with same isin or conid (wkn) and + // currency if (isin.length() > 0 && isin.equals(s.getIsin())) { - if (currency.equals(s.getCurrencyCode())) + if (amount.getCurrencyCode().equals(s.getCurrencyCode())) foundIsinTransaction = true; else if (this.ibAccountCurrency.equals(s.getCurrencyCode())) foundIsinBase = true; } - + else if (conid.length() > 0 && conid.equals(s.getWkn())) + { + if (amount.getCurrencyCode().equals(s.getCurrencyCode())) + foundWknTransaction = true; + else if (this.ibAccountCurrency.equals(s.getCurrencyCode())) + foundIsinBase = true; + } } - if (!foundIsinTransaction && foundIsinBase && element.getAttribute("fxRateToBase").length() > 0) + if ((!foundIsinTransaction || !foundWknTransaction) && foundIsinBase + && element.getAttribute("fxRateToBase").length() > 0) { - amount = amount * Double.parseDouble(element.getAttribute("fxRateToBase")); - currency = asCurrencyUnit(this.ibAccountCurrency); + BigDecimal fxRateToBase = asExchangeRate(element.getAttribute("fxRateToBase")); + + amount = Money.of(ibAccountCurrency, BigDecimal.valueOf(amount.getAmount()) + .multiply(fxRateToBase).setScale(0, RoundingMode.HALF_UP).longValue()); } } } - amount = Math.abs(amount); - setAmount(element, transaction, amount, currency); - + setAmount(element, transaction, amount); transaction.setNote(element.getAttribute("description")); - // Transactions which do not have an account-id will not be imported. + // Transactions which do not have an account-id will not be + // imported. if (!element.getAttribute("accountId").equals("-")) return new TransactionItem(transaction); else @@ -338,7 +300,11 @@ else if (this.ibAccountCurrency.equals(s.getCurrencyCode())) private Function buildPortfolioTransaction = element -> { String assetCategory = element.getAttribute("assetCategory"); - if (!Arrays.asList(ASSETKEY_STOCK, ASSETKEY_OPTION, ASSETKEY_FUTURE_OPTION).contains(assetCategory)) + if (!Arrays.asList(ASSETKEY_STOCK, // + ASSETKEY_OPTION, // + ASSETKEY_FUTURE_OPTION, // + ASSETKEY_CFD) // + .contains(assetCategory)) return null; // Unused Information from Flexstatement Trades, to be used in the @@ -350,6 +316,11 @@ else if (this.ibAccountCurrency.equals(s.getCurrencyCode())) { transaction.setType(PortfolioTransaction.Type.BUY); } + else if (element.getAttribute("buySell").equals("BUY (Ca.)")) + { + transaction.setNote(Messages.MsgErrorOrderCancellationUnsupported); + transaction.setType(PortfolioTransaction.Type.SELL); + } else if (element.getAttribute("buySell").equals("SELL")) { transaction.setType(PortfolioTransaction.Type.SELL); @@ -364,29 +335,36 @@ else if (element.getAttribute("buySell").equals("SELL")) // New format is stored in dateTime, take care for double imports). if (element.hasAttribute("dateTime")) { - transaction.setDate(convertDate(element.getAttribute("dateTime").substring(0, 8), + transaction.setDate(ExtractorUtils.asDate(element.getAttribute("dateTime").substring(0, 8), element.getAttribute("dateTime").substring(9, 15))); } else { if (element.hasAttribute("tradeTime")) - { - transaction.setDate( - convertDate(element.getAttribute("tradeDate"), element.getAttribute("tradeTime"))); - } + transaction.setDate(ExtractorUtils.asDate(element.getAttribute("tradeDate"), + element.getAttribute("tradeTime"))); else - { - transaction.setDate(convertDate(element.getAttribute("tradeDate"), "000000")); - } + transaction.setDate(ExtractorUtils.asDate(element.getAttribute("tradeDate"), "000000")); } // transaction currency - String currency = asCurrencyUnit(element.getAttribute("currency")); + String currency = asCurrencyCode(element.getAttribute("currency")); - // Set the Amount which is "netCash" - Double amount = Math.abs(Double.parseDouble(element.getAttribute("netCash"))); - setAmount(element, transaction.getPortfolioTransaction(), amount, currency); - setAmount(element, transaction.getAccountTransaction(), amount, currency, false); + // Set the Amount which is "netCash" or "cost" + if (element.hasAttribute("cost")) + { + Money amount = Money.of(asCurrencyCode(element.getAttribute("currency")), + asAmount(element.getAttribute("cost"))); + setAmount(element, transaction.getPortfolioTransaction(), amount); + setAmount(element, transaction.getAccountTransaction(), amount, false); + } + else + { + Money amount = Money.of(asCurrencyCode(element.getAttribute("currency")), + asAmount(element.getAttribute("netCash"))); + setAmount(element, transaction.getPortfolioTransaction(), amount); + setAmount(element, transaction.getAccountTransaction(), amount, false); + } // Share Quantity Double qty = Math.abs(Double.parseDouble(element.getAttribute("quantity"))); @@ -394,23 +372,49 @@ else if (element.getAttribute("buySell").equals("SELL")) transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor() * multiplier.doubleValue())); // fees - double fees = Math.abs(Double.parseDouble(element.getAttribute("ibCommission"))); - String feesCurrency = asCurrencyUnit(element.getAttribute("ibCommissionCurrency")); - Unit feeUnit = createUnit(element, Unit.Type.FEE, fees, feesCurrency); + Money fees = Money.of(asCurrencyCode(element.getAttribute("ibCommissionCurrency")), + asAmount(element.getAttribute("ibCommission"))); + Unit feeUnit = createUnit(element, Unit.Type.FEE, fees); transaction.getPortfolioTransaction().addUnit(feeUnit); // taxes - double taxes = Math.abs(Double.parseDouble(element.getAttribute("taxes"))); - Unit taxUnit = createUnit(element, Unit.Type.TAX, taxes, currency); + Money taxes = Money.of(currency, asAmount(element.getAttribute("taxes"))); + Unit taxUnit = createUnit(element, Unit.Type.TAX, taxes); transaction.getPortfolioTransaction().addUnit(taxUnit); transaction.setSecurity(this.getOrCreateSecurity(element, true)); - transaction.setNote(element.getAttribute("description")); + if (transaction.getNote() == null + || !transaction.getNote().equals(Messages.MsgErrorOrderCancellationUnsupported)) + { + // Trade ID + if (!element.getAttribute("tradeID").isEmpty()) + transaction.setNote("Trade-ID: " + element.getAttribute("tradeID")); + + // Transaction ID + if (!element.getAttribute("transactionID").isEmpty()) + { + if (transaction.getNote() != null) + transaction.setNote(transaction.getNote() + " | Transaction-ID: " + + element.getAttribute("transactionID")); + else + transaction.setNote("Transaction-ID: " + element.getAttribute("transactionID")); + } + } ExtractorUtils.fixGrossValueBuySell().accept(transaction); - return new BuySellEntryItem(transaction); + if (transaction.getPortfolioTransaction().getCurrencyCode() != null + && transaction.getPortfolioTransaction().getAmount() != 0) + { + if (transaction.getPortfolioTransaction().getNote() == null || !transaction.getPortfolioTransaction() + .getNote().equals(Messages.MsgErrorOrderCancellationUnsupported)) + return new BuySellEntryItem(transaction); + else + return new NonImportableItem(Messages.MsgErrorOrderCancellationUnsupported); + + } + return null; }; /** @@ -418,7 +422,7 @@ else if (element.getAttribute("buySell").equals("SELL")) * in eElement. */ private Function buildCorporateTransaction = eElement -> { - Money proceeds = Money.of(asCurrencyUnit(eElement.getAttribute("currency")), + Money proceeds = Money.of(asCurrencyCode(eElement.getAttribute("currency")), Values.Amount.factorize(Double.parseDouble(eElement.getAttribute("proceeds")))); if (!proceeds.isZero()) @@ -426,14 +430,12 @@ else if (element.getAttribute("buySell").equals("SELL")) BuySellEntry transaction = new BuySellEntry(); if (Double.parseDouble(eElement.getAttribute("quantity")) >= 0) - { transaction.setType(PortfolioTransaction.Type.BUY); - } else - { transaction.setType(PortfolioTransaction.Type.SELL); - } - transaction.setDate(convertDate(eElement.getAttribute("reportDate"))); + + transaction.setDate(ExtractorUtils.asDate(eElement.getAttribute("reportDate"))); + // Share Quantity double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity"))); transaction.setShares(Values.Share.factorize(qty)); @@ -451,14 +453,12 @@ else if (element.getAttribute("buySell").equals("SELL")) // Set Transaction Type PortfolioTransaction transaction = new PortfolioTransaction(); if (Double.parseDouble(eElement.getAttribute("quantity")) >= 0) - { transaction.setType(PortfolioTransaction.Type.DELIVERY_INBOUND); - } else - { transaction.setType(PortfolioTransaction.Type.DELIVERY_OUTBOUND); - } - transaction.setDateTime(convertDate(eElement.getAttribute("reportDate"))); + + transaction.setDateTime(ExtractorUtils.asDate(eElement.getAttribute("reportDate"))); + // Share Quantity Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity"))); transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor())); @@ -472,118 +472,92 @@ else if (element.getAttribute("buySell").equals("SELL")) } }; - private Unit createUnit(Element element, Unit.Type unitType, Double amount, String currency) + private Unit createUnit(Element element, Unit.Type unitType, Money amount) { Unit unit; - if (ibAccountCurrency == null || ibAccountCurrency.equals(currency)) + if (ibAccountCurrency == null || ibAccountCurrency.equals(amount.getCurrencyCode())) { - unit = new Unit(unitType, Money.of(currency, Values.Amount.factorize(amount))); + unit = new Unit(unitType, amount); } else { - // only required when a account currency is available - String fxRateToBaseString = element.getAttribute("fxRateToBase"); BigDecimal fxRateToBase; + String fxRateToBaseString = element.getAttribute("fxRateToBase"); + if (fxRateToBaseString != null && !fxRateToBaseString.isEmpty()) - { - fxRateToBase = new BigDecimal(fxRateToBaseString).setScale(4, RoundingMode.HALF_DOWN); - } + fxRateToBase = asExchangeRate(fxRateToBaseString); else - { - fxRateToBase = new BigDecimal("1.0000"); - } + fxRateToBase = BigDecimal.ONE; + BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN); - BigDecimal baseCurrencyMoney = BigDecimal.valueOf(amount.doubleValue()) - .setScale(2, RoundingMode.HALF_DOWN).divide(inverseRate, RoundingMode.HALF_DOWN); + Money fxAmount = Money.of(ibAccountCurrency, BigDecimal.valueOf(amount.getAmount()) + .divide(inverseRate, Values.MC).setScale(0, RoundingMode.HALF_UP).longValue()); - unit = new Unit(unitType, - Money.of(ibAccountCurrency, - Math.round(baseCurrencyMoney.doubleValue() * Values.Amount.factor())), - Money.of(currency, Values.Amount.factorize(amount)), fxRateToBase); + unit = new Unit(unitType, fxAmount, amount, fxRateToBase); } return unit; } - private void setAmount(Element element, Transaction transaction, Double amount, String currency) + private void setAmount(Element element, Transaction transaction, Money amount) { - setAmount(element, transaction, amount, currency, true); + setAmount(element, transaction, amount, true); } - private void setAmount(Element element, Transaction transaction, Double amount, String currency, - boolean addUnit) + private void setAmount(Element element, Transaction transaction, Money amount, boolean addUnit) { - if (ibAccountCurrency != null && !ibAccountCurrency.equals(currency)) + if (ibAccountCurrency != null && !ibAccountCurrency.equals(amount.getCurrencyCode())) { - // only required when a account currency is available - String fxRateToBaseString = element.getAttribute("fxRateToBase"); BigDecimal fxRateToBase; + String fxRateToBaseString = element.getAttribute("fxRateToBase"); + if (fxRateToBaseString != null && !fxRateToBaseString.isEmpty()) - { - fxRateToBase = BigDecimal.valueOf(Double.parseDouble(fxRateToBaseString)); - } + fxRateToBase = asExchangeRate(fxRateToBaseString); else - { - fxRateToBase = new BigDecimal(1); - } + fxRateToBase = BigDecimal.ONE; + + Money fxAmount = Money.of(ibAccountCurrency, BigDecimal.valueOf(amount.getAmount()) + .multiply(fxRateToBase).setScale(0, RoundingMode.HALF_UP).longValue()); + + transaction.setMonetaryAmount(fxAmount); - BigDecimal baseCurrencyMoney = BigDecimal.valueOf(amount.doubleValue() * Values.Amount.factor()) - .multiply(fxRateToBase); - transaction.setAmount(Math.round(baseCurrencyMoney.doubleValue())); - transaction.setCurrencyCode(ibAccountCurrency); if (addUnit) { - Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, transaction.getMonetaryAmount(), - Money.of(currency, Math.round(amount.doubleValue() * Values.Amount.factor())), - fxRateToBase); - + Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, fxAmount, amount, fxRateToBase); transaction.addUnit(grossValue); } } else - { - transaction.setAmount(Math.round(amount.doubleValue() * Values.Amount.factor())); - transaction.setCurrencyCode(currency); + transaction.setMonetaryAmount(amount); if (addUnit && transaction.getSecurity() != null - && !transaction.getSecurity().getCurrencyCode().equals(currency)) + && !transaction.getSecurity().getCurrencyCode().equals(amount.getCurrencyCode())) { // 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; + String fxRateToBaseString = element.getAttribute("fxRateToBase"); + if (fxRateToBaseString != null && !fxRateToBaseString.isEmpty()) - { - fxRateToBase = BigDecimal.valueOf(Double.parseDouble(fxRateToBaseString)); - } + fxRateToBase = asExchangeRate(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). + fxRateToBase = BigDecimal.ONE; + BigDecimal inverseRate = BigDecimal.ONE.divide(fxRateToBase, 10, RoundingMode.HALF_DOWN); - BigDecimal securityCurrencyMoney = BigDecimal.valueOf(amount.doubleValue() * Values.Amount.factor()) - .divide(inverseRate, RoundingMode.HALF_DOWN); + Money fxAmount = Money.of(transaction.getSecurity().getCurrencyCode(), + BigDecimal.valueOf(amount.getAmount()).divide(inverseRate, Values.MC) + .setScale(0, RoundingMode.HALF_UP).longValue()); + + transaction.setMonetaryAmount(amount); - // 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); + Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, amount, fxAmount, inverseRate); transaction.addUnit(grossValue); } - } } @@ -603,13 +577,10 @@ private void importModelObjects(String type, Function handleNodeF { Item item = handleNodeFunction.apply((Element) nNode); if (item != null) - { results.add(item); - } } catch (Exception e) { - errors.add(e); } } @@ -651,12 +622,14 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) { // Lookup the Exchange Suffix for Yahoo Optional tickerSymbol = Optional.ofNullable(element.getAttribute("symbol")); + Optional underlyingSymbol = Optional.ofNullable(element.getAttribute("underlyingSymbol")); + Optional underlyingSecurityID = Optional.ofNullable(element.getAttribute("underlyingSecurityID")); String assetCategory = element.getAttribute("assetCategory"); String exchange = element.getAttribute("exchange"); String quoteFeed = QuoteFeed.MANUAL; // yahoo uses '-' instead of ' ' - String currency = asCurrencyUnit(element.getAttribute("currency")); + String currency = asCurrencyCode(element.getAttribute("currency")); String isin = element.getAttribute("isin"); String cusip = element.getAttribute("cusip"); Optional computedTickerSymbol = tickerSymbol.map(t -> t.replaceAll(" ", "-")); @@ -679,7 +652,7 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) if (ASSETKEY_STOCK.equals(assetCategory)) { computedTickerSymbol = tickerSymbol; - if (!"USD".equals(currency)) + if (!CurrencyUnit.USD.equals(currency)) { // some symbols in IB included the exchange as lower key // without "." at the end, e.g. BMWd for BMW trading at d @@ -690,7 +663,7 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) // appended, e.g. Deutsche Bank is DBKEUR, BMW is BMWEUR. // Also notices this during cash transactions (dividend // payments) - if ("EUR".equals(currency)) + if (CurrencyUnit.EUR.equals(currency)) computedTickerSymbol = computedTickerSymbol.map(t -> t.replaceAll("EUR$", "")); // at last, lets add the exchange to the ticker symbol. @@ -700,10 +673,33 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) computedTickerSymbol = computedTickerSymbol.map(t -> t + '.' + exchanges.get(exchange)); } + // For Stock, lets use Alphavante quote feed by default quoteFeed = AlphavantageQuoteFeed.ID; } + if (ASSETKEY_CFD.equals(assetCategory)) + { + quoteFeed = AlphavantageQuoteFeed.ID; + + if (underlyingSecurityID.isPresent()) + isin = underlyingSecurityID.get(); + + if (underlyingSymbol.isPresent()) + { + // use underlyingSymbol instead of symbol for CFD + if (exchanges.containsKey(underlyingSymbol.get())) + { + computedTickerSymbol = Optional.of(exchanges.get(underlyingSymbol.get())); + quoteFeed = YahooFinanceQuoteFeed.ID; + } + else + { + computedTickerSymbol = underlyingSymbol; + } + } + } + Security s2 = null; for (Security s : allSecurities) @@ -728,6 +724,7 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) return null; Security security = new Security(description, isin, computedTickerSymbol.orElse(null), quoteFeed); + // We use the Wkn to store the IB conID as a unique identifier security.setWkn(conID); security.setCurrencyCode(currency); @@ -735,6 +732,7 @@ private Security getOrCreateSecurity(Element element, boolean doCreate) // Store allSecurities.add(security); + // add to result SecurityItem item = new SecurityItem(security); results.add(item); @@ -756,12 +754,12 @@ private void calculateShares(Transaction transaction, Element element) // Regex Pattern matches the Dividend per Share and calculate number // of shares - Pattern dividendPattern = Pattern.compile(".*DIVIDEND.* ([0-9]*\\.[0-9]*) .*"); - Matcher tagmatch = dividendPattern.matcher(desc); - if (tagmatch.find()) + Pattern pDividendPerShares = Pattern.compile(".*DIVIDEND( [\\w]{3})? (?[\\.,\\d]+)( [\\w]{3})? PER SHARE .*"); + Matcher m = pDividendPerShares.matcher(desc); + if (m.find()) { - double dividend = Double.parseDouble(tagmatch.group(1)); - numShares = Math.round(amount / dividend) * Values.Share.factor(); + double dividendPerShares = Double.parseDouble(m.group("dividendPerShares")); + numShares = Math.round(amount / dividendPerShares) * Values.Share.factor(); } transaction.setShares(numShares); @@ -777,4 +775,100 @@ public List getResults() return results; } } + + @Override + public List extract(SecurityCache securityCache, Extractor.InputFile inputFile, List errors) + { + try (FileInputStream in = new FileInputStream(inputFile.getFile())) + { + IBFlexStatementExtractorResult result = importActivityStatement(in); + errors.addAll(result.getErrors()); + return result.getResults(); + } + catch (IOException e) + { + errors.add(e); + return Collections.emptyList(); + } + } + + @Override + public List postProcessing(List items) + { + // Group dividend and taxes transactions together and group by date and + // security + Map>> dividendTaxTransactions = items.stream() // + .filter(TransactionItem.class::isInstance) // + .map(TransactionItem.class::cast) // + .filter(i -> i.getSubject() instanceof AccountTransaction) // + .filter(i -> AccountTransaction.Type.DIVIDENDS // + .equals(((AccountTransaction) i.getSubject()).getType()) || // + AccountTransaction.Type.TAXES // + .equals(((AccountTransaction) i.getSubject()).getType())) // + .collect(Collectors.groupingBy(Item::getDate, Collectors.groupingBy(Item::getSecurity))); + + dividendTaxTransactions.forEach((k, v) -> { + v.forEach((security, transactions) -> { + AccountTransaction dividendTransaction = (AccountTransaction) transactions.get(0).getSubject(); + + if (transactions.size() == 2) + { + + AccountTransaction taxTransaction = (AccountTransaction) transactions.get(1).getSubject(); + + // Which document is the taxes and which the dividend? + if (!AccountTransaction.Type.TAXES.equals(taxTransaction.getType())) + { + dividendTransaction = (AccountTransaction) transactions.get(1).getSubject(); + taxTransaction = (AccountTransaction) transactions.get(0).getSubject(); + } + + // Subtract the tax from the taxes document from the total + // amount. + dividendTransaction.setMonetaryAmount(dividendTransaction.getMonetaryAmount() + .subtract(taxTransaction.getMonetaryAmount())); + + // Add tax as tax unit + dividendTransaction.addUnit(new Unit(Unit.Type.TAX, taxTransaction.getMonetaryAmount())); + + // Set note that the taxes document will be deleted. + taxTransaction.setNote(TO_BE_DELETED); + } + }); + }); + + // iterate list and remove items that are marked TO_BE_DELETED + Iterator iter = items.iterator(); + while (iter.hasNext()) + { + Object o = iter.next().getSubject(); + if (o instanceof AccountTransaction) + { + AccountTransaction a = (AccountTransaction) o; + if (TO_BE_DELETED.equals(a.getNote())) + iter.remove(); + } + } + + return items; + } + + protected String asCurrencyCode(String currency) + { + if (currency == null) + return client.getBaseCurrency(); + + CurrencyUnit unit = CurrencyUnit.getInstance(currency.trim()); + return unit == null ? client.getBaseCurrency() : unit.getCurrencyCode(); + } + + protected long asAmount(String value) + { + return ExtractorUtils.convertToNumberLong(value, Values.Amount, "en", "US"); + } + + protected BigDecimal asExchangeRate(String value) + { + return ExtractorUtils.convertToNumberBigDecimal(value, Values.Share, "en", "US"); + } }