package name.abuchen.portfolio.datatransfer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import name.abuchen.portfolio.Messages;
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;
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;
import name.abuchen.portfolio.online.QuoteFeed;
@SuppressWarnings("nls")
public class IBFlexStatementExtractor implements Extractor
{
private final Client client;
private final List<Item> results;
private List<Security> allSecurities;
private Map<String, String> exchanges;
public IBFlexStatementExtractor(Client client)
{
this.client = client;
this.results = new ArrayList<>();
allSecurities = new ArrayList<>(client.getSecurities());
// Maps Interactive Broker Exchange to Yahoo Exchanges, to be completed
this.exchanges = new HashMap<>();
this.exchanges.put("EBS", "SW");
this.exchanges.put("LSE", "L");
this.exchanges.put("SWX", "SW");
this.exchanges.put("TSE", "TO");
this.exchanges.put("VENTURE", "V");
}
private LocalDate convertDate(String date) throws DateTimeParseException
{
if (date.length() > 8)
{
return LocalDate.parse(date);
}
else
{
return LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd"));
}
}
/**
* Lookup a Security in the Model or create a new one if it does not yet
* exist It uses IB ContractID (conID) for the WKN, tries to degrade if
* conID or ISIN are not available
*/
private Security getOrCreateSecurity(Client client, Element eElement, boolean doCreate)
{
// Lookup the Exchange Suffix for Yahoo
String tickerSymbol = eElement.getAttribute("symbol");
String yahooSymbol = tickerSymbol;
String exchange = eElement.getAttribute("exchange");
String currency = asCurrencyUnit(eElement.getAttribute("currency"));
String isin = eElement.getAttribute("isin");
String cusip = eElement.getAttribute("cusip");
// Store cusip in isin if isin is not available
if (isin.length() == 0 && cusip.length() > 0)
isin = cusip;
String conID = eElement.getAttribute("conid");
String description = eElement.getAttribute("description");
if (tickerSymbol != null)
{
String exch = this.exchanges.get(exchange);
if (exch != null && exch.length() > 0)
yahooSymbol = tickerSymbol + '.' + exch;
}
for (Security s : allSecurities)
{
// Find security with same conID or isin or yahooSymbol
if (conID != null && conID.length() > 0 && conID.equals(s.getWkn()))
return s;
if (isin.length() > 0 && isin.equals(s.getIsin()))
return s;
if (yahooSymbol != null && yahooSymbol.length() > 0 && yahooSymbol.equals(s.getTickerSymbol()))
return s;
}
if (!doCreate)
return null;
Security security = new Security(description, isin, yahooSymbol, QuoteFeed.MANUAL);
// We use the Wkn to store the IB conID as a unique identifier
security.setWkn(conID);
security.setCurrencyCode(currency);
security.setNote(description);
// Store
allSecurities.add(security);
// add to result
SecurityItem item = new SecurityItem(security);
results.add(item);
return security;
}
/**
* Construct a BuySellEntry based on Trade object defined in eElement
*/
private void buildPortfolioTransaction(Client client, Element eElement) throws ParseException
{
// Unused Information from Flexstatement Trades, to be used in the
// future: currency, tradeTime, transactionID, ibOrderID
BuySellEntry transaction = new BuySellEntry();
// Set Transaction Type
if (eElement.getAttribute("buySell").equals("BUY"))
{
transaction.setType(PortfolioTransaction.Type.BUY);
}
else if (eElement.getAttribute("buySell").equals("SELL"))
{
transaction.setType(PortfolioTransaction.Type.SELL);
}
else
{
throw new IllegalArgumentException();
}
String d = eElement.getAttribute("tradeDate");
if (d == null || d.length() == 0)
{
// use reportDate for CorporateActions
d = eElement.getAttribute("reportDate");
}
transaction.setDate(convertDate(d));
// transaction currency
String currency = asCurrencyUnit(eElement.getAttribute("currency"));
// Set the Amount which is "cost"
transaction.setCurrencyCode(currency);
transaction.setAmount(Values.Amount.factorize(Double.parseDouble(eElement.getAttribute("cost"))));
// Share Quantity
Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
// fees
double fees = Math.abs(Double.parseDouble(eElement.getAttribute("ibCommission")));
String feesCurrency = asCurrencyUnit(eElement.getAttribute("ibCommissionCurrency"));
Unit unit = new Unit(Unit.Type.FEE, Money.of(feesCurrency, Values.Amount.factorize(fees)));
transaction.getPortfolioTransaction().addUnit(unit);
// taxes
double taxes = Math.abs(Double.parseDouble(eElement.getAttribute("taxes")));
unit = new Unit(Unit.Type.TAX, Money.of(currency, Values.Amount.factorize(taxes)));
transaction.getPortfolioTransaction().addUnit(unit);
transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
transaction.setNote(eElement.getAttribute("description"));
results.add(new BuySellEntryItem(transaction));
}
/**
* Constructs a Transaction object for a Corporate Transaction defined in
* eElement.
*/
private void buildCorporateTransaction(Client client, Element eElement) throws ParseException
{
Money proceeds = Money.of(asCurrencyUnit(eElement.getAttribute("currency")),
Values.Amount.factorize(Double.parseDouble(eElement.getAttribute("proceeds"))));
if (!proceeds.isZero())
{
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")));
// Share Quantity
double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
transaction.setShares(Values.Share.factorize(qty));
transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
transaction.setNote(eElement.getAttribute("description"));
transaction.setMonetaryAmount(proceeds);
results.add(new BuySellEntryItem(transaction));
}
else
{
// 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.setDate(convertDate(eElement.getAttribute("reportDate")));
// Share Quantity
Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
transaction.setNote(eElement.getAttribute("description"));
transaction.setMonetaryAmount(proceeds);
results.add(new TransactionItem(transaction));
}
}
/**
* Figure out how many shares a dividend payment is related to. Extracts the
* information from the description string given by IB
*/
private void calculateShares(Transaction transaction, Element eElement)
{
// Figure out how many shares were holding related to this Dividend
// Payment
long numShares = 0;
String desc = eElement.getAttribute("description");
double amount = Double.parseDouble(eElement.getAttribute("amount"));
// 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())
{
double dividend = Double.parseDouble(tagmatch.group(1));
numShares = Math.round(amount / dividend) * Values.Share.factor();
}
transaction.setShares(numShares);
}
private void buildAccountTransaction(Client client, Element eElement) throws ParseException
{
AccountTransaction transaction = new AccountTransaction();
transaction.setDate(convertDate(eElement.getAttribute("dateTime")));
Double amount = Double.parseDouble(eElement.getAttribute("amount"));
String currency = asCurrencyUnit(eElement.getAttribute("currency"));
// Set Transaction Type
if (eElement.getAttribute("type").equals("Deposits")
|| eElement.getAttribute("type").equals("Deposits & Withdrawals"))
{
if (amount >= 0)
{
transaction.setType(AccountTransaction.Type.DEPOSIT);
}
else
{
transaction.setType(AccountTransaction.Type.REMOVAL);
}
}
else if (eElement.getAttribute("type").equals("Dividends")
|| eElement.getAttribute("type").equals("Payment In Lieu Of Dividends"))
{
transaction.setType(AccountTransaction.Type.DIVIDENDS);
// Set the Symbol
if (eElement.getAttribute("symbol").length() > 0)
transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
this.calculateShares(transaction, eElement);
}
else if (eElement.getAttribute("type").equals("Withholding Tax"))
{
// Set the Symbol
if (eElement.getAttribute("symbol").length() > 0)
transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
transaction.setType(AccountTransaction.Type.TAXES);
// Temporary until the model supports negative interest rates and
// dividends see #310
throw new ParseException(eElement.getAttribute("dateTime") + " Witholding Tax is not supported", 0);
}
else if (eElement.getAttribute("type").equals("Broker Interest Received"))
{
transaction.setType(AccountTransaction.Type.INTEREST);
}
else if (eElement.getAttribute("type").equals("Broker Interest Paid"))
{
// Temporary until the model supports negative interest see #310
throw new ParseException(eElement.getAttribute("dateTime") + " Broker Interest Paid is not supported", 0);
}
else if (eElement.getAttribute("type").equals("Other Fees"))
{
transaction.setType(AccountTransaction.Type.FEES);
}
else
{
throw new IllegalArgumentException();
}
amount = Math.abs(amount);
transaction.setAmount(Math.round(amount.doubleValue() * Values.Amount.factor()));
transaction.setCurrencyCode(currency);
transaction.setNote(eElement.getAttribute("description"));
results.add(new TransactionItem(transaction));
}
/**
* Imports Trades, CorporateActions and CashTransactions from Document
*/
private void importModelObjects(Document doc, String type, List<Exception> errors)
{
NodeList nList = doc.getElementsByTagName(type);
for (int temp = 0; temp < nList.getLength(); temp++)
{
Node nNode = nList.item(temp);
if (nNode.getNodeType() == Node.ELEMENT_NODE)
{
try
{
if (type.equals("Trade"))
{
this.buildPortfolioTransaction(client, (Element) nNode);
}
else if (type.equals("CorporateAction"))
{
this.buildCorporateTransaction(client, (Element) nNode);
}
else if (type.equals("CashTransaction"))
{
this.buildAccountTransaction(client, (Element) nNode);
}
}
catch (ParseException e)
{
errors.add(e);
}
}
}
}
/**
* Import an Interactive Broker ActivityStatement from an XML file. It
* currently only imports Trades, Corporate Transactions and Cash
* Transactions.
*/
/* package */void importActivityStatement(InputStream f, List<Exception> errors)
{
try
{
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(f);
doc.getDocumentElement().normalize();
// Process all Trades
importModelObjects(doc, "Trade", errors);
// Process all CashTransaction
importModelObjects(doc, "CashTransaction", errors);
// Process all CorporateTransactions
importModelObjects(doc, "CorporateAction", errors);
// TODO: Process all FxTransactions and ConversionRates
}
catch (ParserConfigurationException | SAXException | IOException e)
{
errors.add(e);
}
}
/**
* 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();
}
/* package */List<Item> getResults()
{
return results;
}
@Override
public String getLabel()
{
return Messages.IBXML_Label;
}
@Override
public String getFilterExtension()
{
return "*.xml"; //$NON-NLS-1$
}
@Override
public List<Item> extract(List<File> files, List<Exception> errors)
{
results.clear();
for (File f : files)
{
try
{
importActivityStatement(new FileInputStream(f), errors);
}
catch (FileNotFoundException e)
{
errors.add(e);
}
}
return results;
}
}