|
36 | 36 | import java.io.FileOutputStream; |
37 | 37 | import java.io.IOException; |
38 | 38 | import java.io.InputStream; |
| 39 | +import java.math.BigDecimal; |
| 40 | +import java.math.RoundingMode; |
39 | 41 | import java.nio.file.Files; |
40 | 42 | import java.time.LocalDateTime; |
41 | 43 | import java.util.ArrayList; |
@@ -3554,4 +3556,127 @@ public void testIBFlexStatementFile27() throws IOException |
3554 | 3556 | // Exchange rate should be 0.01 (GBP to GBX: 1 GBP = 100 GBX, so rate is 0.01) |
3555 | 3557 | assertThat(unit.getExchangeRate().compareTo(new java.math.BigDecimal("0.01")), is(0)); |
3556 | 3558 | } |
| 3559 | + |
| 3560 | + @Test |
| 3561 | + public void testIBFlexStatementFile28() throws IOException |
| 3562 | + { |
| 3563 | + // Test cross-rate calculation using fxRateToBase when direct rate is missing |
| 3564 | + // Case 1: Transaction currency: USD, Security currency: GBP, Base currency: EUR |
| 3565 | + // Case 2: Transaction currency: USD, Security currency: GBX (minor unit), Base currency: EUR |
| 3566 | + // USD/EUR available via fxRateToBase, GBP/EUR available via ConversionRate |
| 3567 | + var client = new Client(); |
| 3568 | + |
| 3569 | + // Create security with GBP currency (security currency differs from transaction currency) |
| 3570 | + var hidr = new Security("HSBC MSCI INDONESIA UCITS ET", "GBP"); |
| 3571 | + hidr.setTickerSymbol("HIDR"); |
| 3572 | + hidr.setIsin("IE00B46G8275"); |
| 3573 | + hidr.setWkn("86281326"); |
| 3574 | + client.addSecurity(hidr); |
| 3575 | + |
| 3576 | + // Create security with GBX currency (minor unit) |
| 3577 | + var eqqq = new Security("INVESCO NASDAQ-100 DIST", "GBX"); |
| 3578 | + eqqq.setTickerSymbol("EQQQ"); |
| 3579 | + eqqq.setIsin("IE0032077012"); |
| 3580 | + eqqq.setWkn("18706552"); |
| 3581 | + client.addSecurity(eqqq); |
| 3582 | + |
| 3583 | + var referenceAccount = new Account("A"); |
| 3584 | + referenceAccount.setCurrencyCode("EUR"); |
| 3585 | + client.addAccount(referenceAccount); |
| 3586 | + |
| 3587 | + var extractor = new IBFlexStatementExtractor(client); |
| 3588 | + |
| 3589 | + var activityStatement = getClass().getResourceAsStream("testIBFlexStatementFile28.xml"); |
| 3590 | + var tempFile = createTempFile(activityStatement); |
| 3591 | + |
| 3592 | + var errors = new ArrayList<Exception>(); |
| 3593 | + |
| 3594 | + var results = extractor.extract(Collections.singletonList(tempFile), errors); |
| 3595 | + assertThat(errors, empty()); |
| 3596 | + assertThat(countSecurities(results), is(0L)); // Securities already exist |
| 3597 | + assertThat(countBuySell(results), is(0L)); |
| 3598 | + assertThat(countAccountTransactions(results), is(2L)); |
| 3599 | + |
| 3600 | + // Find the first dividend transaction (HIDR - USD->GBP) |
| 3601 | + List<AccountTransaction> transactions = results.stream() // |
| 3602 | + .filter(TransactionItem.class::isInstance) // |
| 3603 | + .map(item -> (AccountTransaction) ((TransactionItem) item).getSubject()) // |
| 3604 | + .filter(t -> t.getType() == AccountTransaction.Type.DIVIDENDS) // |
| 3605 | + .collect(Collectors.toList()); |
| 3606 | + |
| 3607 | + assertThat(transactions.size(), is(2)); |
| 3608 | + |
| 3609 | + // Test first transaction: HIDR (USD->GBP) |
| 3610 | + AccountTransaction hidrTransaction = transactions.stream() // |
| 3611 | + .filter(t -> "HIDR".equals(t.getSecurity().getTickerSymbol())) // |
| 3612 | + .findFirst() // |
| 3613 | + .orElseThrow(() -> new AssertionError("HIDR dividend transaction not found")); |
| 3614 | + |
| 3615 | + assertThat(hidrTransaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); |
| 3616 | + assertThat(hidrTransaction.getDateTime(), is(LocalDateTime.parse("2025-03-04T00:00"))); |
| 3617 | + |
| 3618 | + assertThat(hidrTransaction.getCurrencyCode(), is("USD")); |
| 3619 | + assertThat(hidrTransaction.getAmount(), is(1815L)); // 18.15 USD |
| 3620 | + assertThat(hidrTransaction.getSecurity().getCurrencyCode(), is("GBP")); |
| 3621 | + |
| 3622 | + // Verify GROSS_VALUE unit exists and has correct conversion |
| 3623 | + // USD/EUR = 0.94106 (from fxRateToBase) |
| 3624 | + // GBP/EUR = 1.2041 (from ConversionRate on 20250304) |
| 3625 | + // USD/GBP = (USD/EUR) / (GBP/EUR) = 0.94106 / 1.2041 ≈ 0.781546 |
| 3626 | + // After inversion in setAmount: GBP/USD ≈ 1.2795 |
| 3627 | + var hidrGrossValueUnit = hidrTransaction.getUnit(Unit.Type.GROSS_VALUE); |
| 3628 | + assertThat("GROSS_VALUE unit should be present for USD->GBP conversion", hidrGrossValueUnit.isPresent(), is(true)); |
| 3629 | + |
| 3630 | + var hidrUnit = hidrGrossValueUnit.get(); |
| 3631 | + assertThat(hidrUnit.getAmount().getCurrencyCode(), is("USD")); |
| 3632 | + assertThat(hidrUnit.getAmount().getAmount(), is(1815L)); // 18.15 USD |
| 3633 | + assertThat(hidrUnit.getForex().getCurrencyCode(), is("GBP")); |
| 3634 | + assertThat(hidrUnit.getForex().getAmount(), is(1419L)); // 14.19 GBP (rounded) |
| 3635 | + |
| 3636 | + BigDecimal hidrExpectedRate = new BigDecimal("1.2795145899"); |
| 3637 | + BigDecimal tolerance = new BigDecimal("0.0000001"); |
| 3638 | + assertThat(hidrUnit.getExchangeRate().subtract(hidrExpectedRate).abs().compareTo(tolerance) < 0, is(true)); |
| 3639 | + |
| 3640 | + // Test second transaction: EQQQ (USD->GBX via GBP) |
| 3641 | + AccountTransaction eqqqTransaction = transactions.stream() // |
| 3642 | + .filter(t -> "EQQQ".equals(t.getSecurity().getTickerSymbol())) // |
| 3643 | + .findFirst() // |
| 3644 | + .orElseThrow(() -> new AssertionError("EQQQ dividend transaction not found")); |
| 3645 | + |
| 3646 | + assertThat(eqqqTransaction.getType(), is(AccountTransaction.Type.DIVIDENDS)); |
| 3647 | + assertThat(eqqqTransaction.getDateTime(), is(LocalDateTime.parse("2025-03-21T00:00"))); |
| 3648 | + |
| 3649 | + assertThat(eqqqTransaction.getCurrencyCode(), is("USD")); |
| 3650 | + assertThat(eqqqTransaction.getAmount(), is(136L)); // 1.36 USD |
| 3651 | + assertThat(eqqqTransaction.getSecurity().getCurrencyCode(), is("GBX")); |
| 3652 | + |
| 3653 | + // Verify GROSS_VALUE unit exists and has correct conversion |
| 3654 | + // USD/EUR = 0.92464 (from fxRateToBase) |
| 3655 | + // GBP/EUR = 1.1726 (from ConversionRate on 20250321) |
| 3656 | + // USD/GBP = (USD/EUR) / (GBP/EUR) = 0.92464 / 1.1726 ≈ 0.7887 |
| 3657 | + // GBX/GBP = 0.01 (from FixedExchangeRateProvider) |
| 3658 | + // USD/GBX = USD/GBP / GBX/GBP = 0.7887 / 0.01 = 78.87 |
| 3659 | + // After inversion in setAmount: GBX/USD ≈ 0.01268 |
| 3660 | + var eqqqGrossValueUnit = eqqqTransaction.getUnit(Unit.Type.GROSS_VALUE); |
| 3661 | + assertThat("GROSS_VALUE unit should be present for USD->GBX conversion", eqqqGrossValueUnit.isPresent(), is(true)); |
| 3662 | + |
| 3663 | + var eqqqUnit = eqqqGrossValueUnit.get(); |
| 3664 | + assertThat(eqqqUnit.getAmount().getCurrencyCode(), is("USD")); |
| 3665 | + assertThat(eqqqUnit.getAmount().getAmount(), is(136L)); // 1.36 USD |
| 3666 | + assertThat(eqqqUnit.getForex().getCurrencyCode(), is("GBX")); |
| 3667 | + assertThat(eqqqUnit.getForex().getAmount(), is(10724L)); // 107.24 GBX (rounded) |
| 3668 | + |
| 3669 | + // Exchange rate calculation (with 10 decimal precision, HALF_DOWN in divisions): |
| 3670 | + // USD/EUR = 0.92464, GBP/EUR = 1.1726 |
| 3671 | + // USD/GBP = 0.92464 / 1.1726 = 0.7887030844... (exact) |
| 3672 | + // With 10 decimals HALF_DOWN: 0.7887030844 |
| 3673 | + // USD/GBX = 0.7887030844 / 0.01 = 78.87030844 |
| 3674 | + // After inversion in setAmount (10 decimals, HALF_DOWN): GBX/USD = 1 / 78.87030844 |
| 3675 | + // Calculate expected rate with same precision as implementation |
| 3676 | + BigDecimal usdToGbp = new BigDecimal("0.92464").divide(new BigDecimal("1.1726"), 10, RoundingMode.HALF_DOWN); |
| 3677 | + BigDecimal usdToGbx = usdToGbp.divide(new BigDecimal("0.01"), 10, RoundingMode.HALF_DOWN); |
| 3678 | + BigDecimal eqqqExpectedRate = BigDecimal.ONE.divide(usdToGbx, 10, RoundingMode.HALF_DOWN); |
| 3679 | + BigDecimal eqqqTolerance = new BigDecimal("0.0000001"); // Same tolerance as HIDR test |
| 3680 | + assertThat(eqqqUnit.getExchangeRate().subtract(eqqqExpectedRate).abs().compareTo(eqqqTolerance) < 0, is(true)); |
| 3681 | + } |
3557 | 3682 | } |
0 commit comments