Skip to content

Commit

Permalink
feat: automatically adapt symbol changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafael Augusto de Oliveira committed Dec 20, 2024
1 parent 88bf20a commit 5f32ab4
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 25 deletions.
47 changes: 44 additions & 3 deletions src/tastytrade_ghostfolio/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from itertools import batched

from tastytrade import Account, ProductionSession
from tastytrade.account import Transaction

Expand Down Expand Up @@ -29,22 +31,56 @@ def filter_trades(transactions: list[Transaction]) -> list[Transaction]:
for transaction in transactions:
if transaction.transaction_type == "Trade" or (
transaction.transaction_type == "Receive Deliver"
and transaction.transaction_sub_type == "Dividend"
and (
transaction.transaction_sub_type == "Dividend"
or transaction.transaction_sub_type == "Symbol Change"
)
):
trades.append(transaction)

return trades


def adapt_symbols(
activities: list[GhostfolioActivity], symbol_mappings: dict[str, str]
) -> list[GhostfolioActivity]:
activities: list[Transaction] | list[GhostfolioActivity],
symbol_mappings: dict[str, str],
) -> list[Transaction] | list[GhostfolioActivity]:
for activity in activities:
activity.symbol = symbol_mappings.get(activity.symbol, activity.symbol)

return activities


def extract_symbol_changes(
transactions: list[Transaction],
) -> tuple[list[Transaction], list[Transaction]]:
symbol_changes = [
transaction
for transaction in transactions
if transaction.transaction_sub_type == "Symbol Change"
]

if symbol_changes:
for change in symbol_changes:
transactions.remove(change)

return transactions, symbol_changes


def adapt_symbol_changes(
transactions: list[Transaction], symbol_changes: list[Transaction]
) -> list[Transaction]:
symbol_changes = sorted(symbol_changes, key=lambda x: x.transaction_date)

symbol_changes_mapping = {}
for changes in batched(symbol_changes, 2):
old_symbol = next(filter(lambda x: x.action == "Sell to Close", changes))
new_symbol = next(filter(lambda x: x.action == "Buy to Open", changes))
symbol_changes_mapping[old_symbol.symbol] = new_symbol.symbol

return adapt_symbols(transactions, symbol_changes_mapping)


if __name__ == "__main__":
ghostfolio_service = GhostfolioService()
ghostfolio_accounts = ghostfolio_service.get_all_accounts()
Expand All @@ -65,6 +101,11 @@ def adapt_symbols(
transactions = get_tastytrade_account_history(session)

trades = filter_trades(transactions)
trades, symbol_changes = extract_symbol_changes(trades)
if symbol_changes:
print("Adapting symbol changes...")
trades = adapt_symbol_changes(trades, symbol_changes)

activities = adapt_trades(trades)

for activity in activities:
Expand Down
134 changes: 128 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def trade_buy() -> Transaction:
return Transaction.parse_obj(
{
"id": 261794408,
"account_number": "5WW40512",
"account_number": "6VV78917",
"transaction_type": "Trade",
"transaction_sub_type": "Buy to Open",
"description": "Bought 1 CCJ @ 40.40",
Expand Down Expand Up @@ -65,7 +65,7 @@ def dividends() -> list[Transaction]:
return [
Transaction.parse_obj(
{
"account_number": "5WW40512",
"account_number": "6VV78917",
"action": None,
"agency_price": None,
"clearing_fees": None,
Expand Down Expand Up @@ -116,7 +116,7 @@ def dividends() -> list[Transaction]:
),
Transaction.parse_obj(
{
"account_number": "5WW40512",
"account_number": "6VV78917",
"action": None,
"agency_price": None,
"clearing_fees": None,
Expand Down Expand Up @@ -172,7 +172,7 @@ def dividends() -> list[Transaction]:
def dividend_reinvestment_transaction_buy() -> list[Transaction]:
return Transaction.parse_obj(
{
"account_number": "5WW40512",
"account_number": "6VV78917",
"action": "Buy to Open",
"agency_price": None,
"clearing_fees": None,
Expand Down Expand Up @@ -229,7 +229,7 @@ def divident_reinvestment(dividend_reinvestment_transaction_buy) -> list[Transac
dividend_reinvestment_transaction_buy,
Transaction.parse_obj(
{
"account_number": "5WW40512",
"account_number": "6VV78917",
"action": None,
"agency_price": None,
"clearing_fees": None,
Expand Down Expand Up @@ -282,5 +282,127 @@ def divident_reinvestment(dividend_reinvestment_transaction_buy) -> list[Transac


@fixture
def transactions(dividends, divident_reinvestment, trade_buy) -> list[Transaction]:
def symbol_change_sell_old() -> Transaction:
return Transaction.parse_obj(
{
"account_number": "6VV78917",
"action": "Sell to Close",
"agency_price": None,
"clearing_fees": None,
"clearing_fees_effect": None,
"commission": None,
"commission_effect": None,
"cost_basis_reconciliation_date": None,
"description": "Symbol change: Close 5.47124 EURN",
"destination_venue": None,
"exchange": None,
"exchange_affiliation_identifier": None,
"exec_id": None,
"executed_at": datetime.datetime(
2024, 7, 15, 10, 0, tzinfo=datetime.timezone.utc
),
"ext_exchange_order_number": None,
"ext_exec_id": None,
"ext_global_order_number": None,
"ext_group_fill_id": None,
"ext_group_id": None,
"id": 820695443,
"instrument_type": InstrumentType.EQUITY,
"is_estimated_fee": True,
"leg_count": None,
"lots": None,
"net_value": Decimal("82.165"),
"net_value_effect": PriceEffect.CREDIT,
"order_id": None,
"other_charge": None,
"other_charge_description": None,
"other_charge_effect": None,
"price": None,
"principal_price": None,
"proprietary_index_option_fees": None,
"proprietary_index_option_fees_effect": None,
"quantity": Decimal("5.47124"),
"regulatory_fees": None,
"regulatory_fees_effect": None,
"reverses_id": None,
"symbol": "KO",
"transaction_date": datetime.date(2024, 7, 15),
"transaction_sub_type": "Symbol Change",
"transaction_type": "Receive Deliver",
"underlying_symbol": "KO",
"value": Decimal("82.165"),
"value_effect": PriceEffect.CREDIT,
}
)


@fixture
def symbol_change_buy_new() -> Transaction:
return Transaction.parse_obj(
{
"account_number": "6VV78917",
"action": "Buy to Open",
"agency_price": None,
"clearing_fees": None,
"clearing_fees_effect": None,
"commission": None,
"commission_effect": None,
"cost_basis_reconciliation_date": None,
"description": "Symbol change: Open 5.47124 CMBT",
"destination_venue": None,
"exchange": None,
"exchange_affiliation_identifier": None,
"exec_id": None,
"executed_at": datetime.datetime(
2024, 7, 15, 10, 0, tzinfo=datetime.timezone.utc
),
"ext_exchange_order_number": None,
"ext_exec_id": None,
"ext_global_order_number": None,
"ext_group_fill_id": None,
"ext_group_id": None,
"id": 820695444,
"instrument_type": InstrumentType.EQUITY,
"is_estimated_fee": True,
"leg_count": None,
"lots": None,
"net_value": Decimal("82.165"),
"net_value_effect": PriceEffect.DEBIT,
"order_id": None,
"other_charge": None,
"other_charge_description": None,
"other_charge_effect": None,
"price": None,
"principal_price": None,
"proprietary_index_option_fees": None,
"proprietary_index_option_fees_effect": None,
"quantity": Decimal("5.47124"),
"regulatory_fees": None,
"regulatory_fees_effect": None,
"reverses_id": None,
"symbol": "COLA",
"transaction_date": datetime.date(2024, 7, 15),
"transaction_sub_type": "Symbol Change",
"transaction_type": "Receive Deliver",
"underlying_symbol": "COLA",
"value": Decimal("82.165"),
"value_effect": PriceEffect.DEBIT,
}
)


@fixture
def symbol_change(symbol_change_sell_old, symbol_change_buy_new) -> list[Transaction]:
return [symbol_change_sell_old, symbol_change_buy_new]


@fixture
def symbol_changeless_transactions(
dividends, divident_reinvestment, trade_buy
) -> list[Transaction]:
return [*dividends, *divident_reinvestment, trade_buy]


@fixture
def transactions(symbol_changeless_transactions, symbol_change) -> list[Transaction]:
return symbol_changeless_transactions + symbol_change
67 changes: 51 additions & 16 deletions tests/unit/test_tastytrade.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
# from tastytrade_ghostfolio.main import filter_dividends
from tastytrade_ghostfolio.main import (
adapt_symbol_changes,
extract_symbol_changes,
filter_trades,
)


# def test_filter_dividends_happy_path(transactions, dividends):
# result = filter_dividends(transactions)
class TestFilterTrades:
def test_should_filter_dividend_reinvestments(
self, transactions, dividend_reinvestment_transaction_buy
):
result = filter_trades(transactions)

# assert len(result) == 2
# assert result == dividends
from tastytrade_ghostfolio.main import filter_trades
assert dividend_reinvestment_transaction_buy in result

def test_should_filter_buy_trades(self, transactions, trade_buy):
result = filter_trades(transactions)

def test_filter_trades_should_filter_dividend_reinvestments(
transactions, dividend_reinvestment_transaction_buy
):
result = filter_trades(transactions)
assert trade_buy in result

assert len(result) == 2
assert dividend_reinvestment_transaction_buy in result
def test_should_filter_symbol_changes(
self, transactions, symbol_change_sell_old, symbol_change_buy_new
):
result = filter_trades(transactions)

assert symbol_change_sell_old in result
assert symbol_change_buy_new in result

def test_filter_trades_should_filter_buy_trades(transactions, trade_buy):
result = filter_trades(transactions)

assert len(result) == 2
assert trade_buy in result
class TestExtractSymbolChanges:
def test_when_theres_symbol_change_should_return_transactions_and_changes_separately(
self, transactions, symbol_changeless_transactions, symbol_change
):
clean_transactions, symbol_changes = extract_symbol_changes(transactions)

assert clean_transactions == symbol_changeless_transactions
assert symbol_changes == symbol_change

def test_when_theres_no_symbol_change_should_return_all_transactions_and_empty_list(
self, symbol_changeless_transactions
):
clean_transactions, symbol_changes = extract_symbol_changes(
symbol_changeless_transactions
)

assert clean_transactions == symbol_changeless_transactions
assert symbol_changes == []


class TestAdaptSymbolChanges:
def test_should_map_old_symbol_to_new_one(
self, symbol_changeless_transactions, symbol_change
):
result = adapt_symbol_changes(symbol_changeless_transactions, symbol_change)

assert all(
transaction.symbol == "COLA"
for transaction in result
if transaction.transaction_sub_type == "Dividend"
)

0 comments on commit 5f32ab4

Please sign in to comment.