Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JSON File #107

Merged
merged 3 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,43 @@ def custom_init(self):
self.filename_pattern_def = ".*History"
self.date_format = "%m/%d/%Y"
self.header_identifier = ""
self.column_labels_line = (
"Run Date,Action,Symbol,Description,Type,Quantity,Price ($),Commission ($),Fees ($),Accrued Interest ($),Amount ($),Cash Balance ($),Settlement Date"
)
self.column_labels_line = "Run Date,Action,Symbol,Description,Type,Quantity,Price ($),Commission ($),Fees ($),Accrued Interest ($),Amount ($),Cash Balance ($),Settlement Date"
self.header_map = {
"Run Date": "date",
"Action": "memo",
"Symbol": "security",
"Amount ($)": "amount",
"Settlement Date": "settleDate",
"Quantity": "units",
"Run Date": "date",
"Action": "memo",
"Symbol": "security",
"Amount ($)": "amount",
"Settlement Date": "settleDate",
"Quantity": "units",
"Accrued Interest ($)": "accrued_interest",
"Fees ($)": "fees",
"Commission ($)": "commission",
"Cash Balance ($)": "balance",
"Price ($)": "unit_price",
"Fees ($)": "fees",
"Commission ($)": "commission",
"Cash Balance ($)": "balance",
"Price ($)": "unit_price",
}
self.transaction_type_map = {
"DIVIDEND RECEIVED": "dividends",
"TRANSFERRED FROM": "cash",
"YOU BOUGHT": "buystock",
"YOU SOLD": "sellstock",
"DIVIDEND RECEIVED": "dividends",
"TRANSFERRED FROM": "cash",
"YOU BOUGHT": "buystock",
"YOU SOLD": "sellstock",
}
self.skip_transaction_types = []
# fmt: on

def deep_identify(self, file):
last_four = self.config.get("account_number", "")[-4:]
return re.match(self.header_identifier, file.head(), flags=re.DOTALL) and f"{last_four}" in file.name
return (
re.match(self.header_identifier, file.head(), flags=re.DOTALL)
and f"{last_four}" in file.name
)

def prepare_table(self, rdr):
for field in ["Action", "Symbol", "Description"]:
rdr = rdr.convert(field, lambda x: x.lstrip())

rdr = rdr.addfield("total", lambda x: x["Amount ($)"])
rdr = rdr.addfield("tradeDate", lambda x: x["Run Date"])
rdr = rdr.cutout('Type')
rdr = rdr.cutout("Type")
rdr = rdr.capture("Action", "(\\S+(?:\\s+\\S+)?)", ["type"], include_original=True)

# for field in ["memo"]:
Expand Down
113 changes: 71 additions & 42 deletions beancount_reds_importers/importers/ibkr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@
"""

import datetime

from beancount.core.number import D

from beancount_reds_importers.libreader import xmlreader
from beancount_reds_importers.libtransactionbuilder import investments
from beancount.core.number import D


class DictToObject:
Expand All @@ -106,8 +108,8 @@ def __init__(self, dictionary):

# xml on left, ofx on right
ofx_type_map = {
'BUY': 'buystock',
'SELL': 'selltock',
"BUY": "buystock",
"SELL": "selltock",
}


Expand All @@ -119,92 +121,119 @@ def custom_init(self):
self.max_rounding_error = 0.04
self.filename_pattern_def = "ibkr"
self.custom_init_run = True
self.date_format = '%Y-%m-%d'
self.date_format = "%Y-%m-%d"
self.get_ticker_info = self.get_ticker_info_from_id

def deep_identify(self, file):
try:
if self.config.get('account_number', None):
if self.config.get("account_number", None):
# account number specific matching
return self.config['account_number'] == list(self.get_xpath_elements("/FlexQueryResponse/FlexStatements/FlexStatement/AccountInformation"))[0]['accountId']
return (
self.config["account_number"]
== list(
self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/AccountInformation"
)
)[0]["accountId"]
)
else:
# base check: simply ensure this looks like a valid IBKR Flex Query file
return list(self.get_xpath_elements("/FlexQueryResponse"))[0] is not None
except IndexError:
return False

def set_currency(self):
self.currency = list(self.get_xpath_elements("/FlexQueryResponse/FlexStatements/FlexStatement/AccountInformation"))[0]['currency']
self.currency = list(
self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/AccountInformation"
)
)[0]["currency"]

# fixup dates
def convert_date(self, d):
d = d.split(' ')[0]
d = d.split(" ")[0]
return datetime.datetime.strptime(d, self.date_format)

def xml_transfer_interpreter(self, xml_data):
# map, with ofx fields on the left and xml fields on the right
ofx_dict = {
'security': xml_data['isin'],
'tradeDate': self.convert_date(xml_data['dateTime']),
'units': D(xml_data['quantity']),
'memo': 'Transfer in kind',
'type': 'transfer',
"security": xml_data["isin"],
"tradeDate": self.convert_date(xml_data["dateTime"]),
"units": D(xml_data["quantity"]),
"memo": "Transfer in kind",
"type": "transfer",
}
return DictToObject(ofx_dict)

def xml_trade_interpreter(self, xml_data):
# map, with ofx fields on the left and xml fields on the right
ofx_dict = {
'security': xml_data['isin'],
'tradeDate': self.convert_date(xml_data['dateTime']),
'memo': xml_data['transactionType'],
'type': ofx_type_map[xml_data['buySell']],
'units': D(xml_data['quantity']),
'unit_price': D(xml_data['tradePrice']),
'commission': -1 * D(xml_data['ibCommission']),
'total': D(xml_data['netCash']),
"security": xml_data["isin"],
"tradeDate": self.convert_date(xml_data["dateTime"]),
"memo": xml_data["transactionType"],
"type": ofx_type_map[xml_data["buySell"]],
"units": D(xml_data["quantity"]),
"unit_price": D(xml_data["tradePrice"]),
"commission": -1 * D(xml_data["ibCommission"]),
"total": D(xml_data["netCash"]),
}
return DictToObject(ofx_dict)

def xml_cash_interpreter(self, xml_data):
# map, with ofx fields on the left and xml fields on the right
ofx_dict = {
'tradeDate': self.convert_date(xml_data['dateTime']),
'amount': D(xml_data['amount']),
'security': xml_data.get('isin', None),
'type': 'cash',
'memo': xml_data['type'],
"tradeDate": self.convert_date(xml_data["dateTime"]),
"amount": D(xml_data["amount"]),
"security": xml_data.get("isin", None),
"type": "cash",
"memo": xml_data["type"],
}

if xml_data['type'] == 'Dividends':
ofx_dict['type'] = 'dividends'
ofx_dict['total'] = ofx_dict['amount']
if xml_data["type"] == "Dividends":
ofx_dict["type"] = "dividends"
ofx_dict["total"] = ofx_dict["amount"]

return DictToObject(ofx_dict)

def get_transactions(self):
yield from self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/Trades/Trade',
xml_interpreter=self.xml_trade_interpreter)
yield from self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/CashTransactions/CashTransaction',
xml_interpreter=self.xml_cash_interpreter)
yield from self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/Transfers/Transfer',
xml_interpreter=self.xml_transfer_interpreter)
yield from self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/Trades/Trade",
xml_interpreter=self.xml_trade_interpreter,
)
yield from self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/CashTransactions/CashTransaction",
xml_interpreter=self.xml_cash_interpreter,
)
yield from self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/Transfers/Transfer",
xml_interpreter=self.xml_transfer_interpreter,
)

def get_balance_assertion_date(self):
ac = list(self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/CashReport/CashReportCurrency'))[0]
return self.convert_date(ac['toDate']).date()
ac = list(
self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/CashReport/CashReportCurrency"
)
)[0]
return self.convert_date(ac["toDate"]).date()

def get_available_cash(self, settlement_fund_balance=0):
"""Assumes there's only one cash currency.
TODO: get investments transaction builder to accept date from get_available_cash
"""
ac = list(self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/CashReport/CashReportCurrency'))[0]
return D(ac['slbNetCash'])
ac = list(
self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/CashReport/CashReportCurrency"
)
)[0]
return D(ac["slbNetCash"])

def get_balance_positions(self):
for pos in self.get_xpath_elements('/FlexQueryResponse/FlexStatements/FlexStatement/OpenPositions/OpenPosition'):
for pos in self.get_xpath_elements(
"/FlexQueryResponse/FlexStatements/FlexStatement/OpenPositions/OpenPosition"
):
balance = {
'security': pos['isin'],
'units': D(pos['position']),
"security": pos["isin"],
"units": D(pos["position"]),
}
yield DictToObject(balance)
32 changes: 16 additions & 16 deletions beancount_reds_importers/importers/ibkr/flexquery_download.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
#!/usr/bin/env python3
"""IBKR Flex Query Downloader"""

import requests
import click
import requests


@click.command()
@click.argument('token', required=True)
@click.argument('query_id', required=True)
@click.argument("token", required=True)
@click.argument("query_id", required=True)
def flexquery_download(token, query_id):
url = "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest"

url = (
"https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest"
)

# Request Flex Query
request_payload = {
"v": "3",
"t": token,
"q": query_id
}

request_payload = {"v": "3", "t": token, "q": query_id}

response = requests.post(url, data=request_payload)

if response.status_code == 200:
request_id = response.text.split("<ReferenceCode>")[1].split("</ReferenceCode>")[0]
# print(f"Request ID: {request_id}")

# Construct URL to get the query result
result_url = f"https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.GetStatement?q={request_id}&t={token}&v=3"

result_response = requests.get(result_url)

if result_response.status_code == 200:
print(result_response.text)
else:
Expand All @@ -37,5 +36,6 @@ def flexquery_download(token, query_id):
print(f"Failed to request the query. Status Code: {response.status_code}")
return None

if __name__ == '__main__':

if __name__ == "__main__":
flexquery_download()
2 changes: 1 addition & 1 deletion beancount_reds_importers/importers/vanguard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def cleanup_memo(self, ot):
# some vanguard files have memos repeated like this:
# 'DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT'
retval = ot.memo
if ot.memo[: int(len(ot.memo) / 2)] == ot.memo[int(len(ot.memo) / 2):]:
if ot.memo[: int(len(ot.memo) / 2)] == ot.memo[int(len(ot.memo) / 2) :]:
retval = ot.memo[: int(len(ot.memo) / 2)]
return retval

Expand Down
Loading