Skip to content

Commit 5251503

Browse files
Added operation id (opid) and sub operation id (subopid) to the ledger.
- Added operation id (`opid`) and sub operation id (`subopid`) to keep track of operations that go together and the order in which they happened. - Changed `_add_row()` to `_add_rows()`. - In `_add_rows()`, allowed with the `data` parameter to either add one single row with a dict or multiple rows with a list of dicts. - `_add_rows()` now returns the `opid` it added and the pd.DataFrame for the rows appended. - Operations return what `_add_rows()` returned, allowing for easier debugging.
1 parent 0c91f9a commit 5251503

File tree

2 files changed

+144
-105
lines changed

2 files changed

+144
-105
lines changed

TODO.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
# TODO LIST
22
- Implement operations
33
- Maybe the operation column in the ledger should be called differently, perhaps action. The reason being that operations might be the things that are done via the API and they might hace two events happening such as sell (selling and univesting).
4-
- Add a new column for operation id and sub id?
5-
- For instance, a sell is an univest and a sell, so both ops should have an id
6-
e.g. 132 and maybe a sub id 1 and 2
7-
these could go on different columns or in one column (132-1 and 132-2)
8-
- The function `_add_row()` should be _add_rows (plural) to allow operation tracking and multiple rows would have the same operation id and multiple sub ids.
9-
- _add_rows should return the operation id (the main one, not sub ids)
104
- Add validation for _add_rows
115
- Change column name `stated_total` to `total`, this makes more sense as it will be calculated and not stated.
126
- In _cols_operation* the grouping should include price_in as the computation of the same instrument with different reference instrument (price_in) wouldn't make sense (i.e. a stock bought in USD and also in CHF wouldn't allow for the computation to be consistent between the two). In the case that would be needed to be done somehow, a conversion would have to happen.

lib/simple_portfolio_ledger/src/simple_portfolio_ledger.py

Lines changed: 144 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
class SimplePortfolioLedger:
1515

1616
_ledger_columns = (
17+
'opid',
18+
'subopid',
1719
'date_execution',
1820
'operation',
1921
'instrument',
@@ -37,6 +39,8 @@ class SimplePortfolioLedger:
3739
# Column description in a dict
3840
_ledger_columns_attrs = {
3941
'column notes': {
42+
'opid': 'Operation id, used to keep track of multiple rows operations.',
43+
'subopid': 'Sub operation id, used to keep track of sub operations.',
4044
'date_execution': 'The date the transaction was actually done.',
4145
'date_order': 'The date the transaction was created.',
4246
#
@@ -686,14 +690,59 @@ def rm_empty_rows(self):
686690
# MARK: OPERATIONS
687691
# *****************
688692

689-
def _add_row(self, value_dict):
693+
def _add_rows(self, data: dict | List[dict]) -> tuple[int, pd.DataFrame]:
694+
"""Add one or multiple rows to the ledger.
695+
696+
The new row(s) will have an opid incremented by one from the max opid in the ledger. Or it will be 0 if max is a nan.
697+
698+
Parameters
699+
----------
700+
data : dict | List[dict]
701+
The data to be added to the ledger. If a dict, the function adds only one row. If a list of dicts, the function adds as many items in the list.
702+
703+
Returns
704+
-------
705+
tuple[int, pd.DataFrame]
706+
Returns a tuple of the inserted opid and the pd.DataFrame inserted.
707+
708+
Raises
709+
------
710+
ValueError
711+
If data is a list and an item in that list is not a dict, an error is raised.
712+
ValueError
713+
If data is neither a dict nor a List, an error is raised.
714+
"""
715+
row_list = []
716+
717+
# Get the last opid and set the one used for the next row
718+
last_opid = self._ledger_df['opid'].max()
719+
if pd.isna(last_opid):
720+
new_op_id = 0
721+
else:
722+
new_op_id = int(last_opid) + 1
723+
724+
# Fill the array of rows to be added
725+
if isinstance(data, dict):
726+
row_list.append({**data, 'opid': new_op_id, 'subopid': 0})
727+
elif isinstance(data, list):
728+
for i, vd in enumerate(data):
729+
if isinstance(vd, dict):
730+
row_list.append({**vd, 'opid': new_op_id, 'subopid': i})
731+
else:
732+
raise ValueError('When creating a row, the item in the list is not a dict.')
733+
else:
734+
raise ValueError('Unsupported type for value_dict, must be a dict or a list of dicts.')
735+
736+
# Add rows to the ledger
737+
# Specifying columns order, `*` needed because self._ledger_columns is a tuple
738+
new_rows = pd.DataFrame(row_list)[[*self._ledger_columns]]
690739
self._ledger_df = (
691-
pd.concat([self._ledger_df, pd.DataFrame([value_dict])])
740+
pd.concat([self._ledger_df, new_rows])
692741
# Make the index coherent (else index might have duplicates)
693742
.reset_index(drop=True)
694743
)
744+
return new_op_id, new_rows
695745

696-
# This a two row operation
697746
def buy(
698747
self,
699748
date_execution,
@@ -704,7 +753,7 @@ def buy(
704753
account,
705754
commission=0,
706755
tax=0,
707-
stated_total=None,
756+
stated_total=None, # Used to verify if inner calculation is correct
708757
date_order=None,
709758
notes='',
710759
commission_notes='',
@@ -780,9 +829,7 @@ def buy(
780829
op_1['account'] = account # invest
781830
op_2['account'] = account # buy
782831

783-
# Create rows
784-
self._add_row(op_1) # invest
785-
self._add_row(op_2) # buy
832+
return self._add_rows(data=[op_1, op_2])
786833

787834
def deposit(
788835
self,
@@ -814,28 +861,28 @@ def deposit(
814861
notes : str, optional
815862
Deposit notes. By default ''.
816863
"""
817-
self._add_row( # deposit
818-
{
819-
'date_execution': date_execution,
820-
'operation': 'deposit',
821-
'instrument': instrument,
822-
'origin': '',
823-
'destination': instrument,
824-
'price_in': instrument,
825-
'price': 1,
826-
'price_w_expenses': 1,
827-
'size': amount,
828-
'commission': 0,
829-
'tax': 0,
830-
'stated_total': amount,
831-
'date_order': (date_order if date_order is not None else date_execution),
832-
'description': f'Deposit {instrument}.',
833-
'notes': notes,
834-
'commission_notes': '',
835-
'tax_notes': '',
836-
'account': account,
837-
}
838-
)
864+
op_deposit = {
865+
'date_execution': date_execution,
866+
'operation': 'deposit',
867+
'instrument': instrument,
868+
'origin': '',
869+
'destination': instrument,
870+
'price_in': instrument,
871+
'price': 1,
872+
'price_w_expenses': 1,
873+
'size': amount,
874+
'commission': 0,
875+
'tax': 0,
876+
'stated_total': amount,
877+
'date_order': (date_order if date_order is not None else date_execution),
878+
'description': f'Deposit {instrument}.',
879+
'notes': notes,
880+
'commission_notes': '',
881+
'tax_notes': '',
882+
'account': account,
883+
}
884+
885+
return self._add_rows(op_deposit)
839886

840887
def dividend(
841888
self,
@@ -864,28 +911,28 @@ def dividend(
864911
)
865912

866913
price = amount / size
867-
self._add_row( # dividend
868-
{
869-
'date_execution': date_execution,
870-
'operation': 'dividend',
871-
'instrument': instrument_received,
872-
'origin': instrument_from,
873-
'destination': '',
874-
'price_in': instrument_received,
875-
'price': price,
876-
'price_w_expenses': price,
877-
'size': size,
878-
'commission': 0,
879-
'tax': 0,
880-
'stated_total': amount,
881-
'date_order': (date_order if date_order is not None else date_execution),
882-
'description': f'Dividend from {instrument_from}.',
883-
'notes': notes,
884-
'commission_notes': '',
885-
'tax_notes': '',
886-
'account': account,
887-
}
888-
)
914+
op_dividend = {
915+
'date_execution': date_execution,
916+
'operation': 'dividend',
917+
'instrument': instrument_received,
918+
'origin': instrument_from,
919+
'destination': '',
920+
'price_in': instrument_received,
921+
'price': price,
922+
'price_w_expenses': price,
923+
'size': size,
924+
'commission': 0,
925+
'tax': 0,
926+
'stated_total': amount,
927+
'date_order': (date_order if date_order is not None else date_execution),
928+
'description': f'Dividend from {instrument_from}.',
929+
'notes': notes,
930+
'commission_notes': '',
931+
'tax_notes': '',
932+
'account': account,
933+
}
934+
935+
return self._add_rows(op_dividend)
889936

890937
def sell(
891938
self,
@@ -897,7 +944,7 @@ def sell(
897944
account,
898945
commission=0,
899946
tax=0,
900-
stated_total=None,
947+
stated_total=None, # Used to verify if inner calculation is correct
901948
date_order=None,
902949
notes='',
903950
commission_notes='',
@@ -973,9 +1020,7 @@ def sell(
9731020
op_1['account'] = account # sell
9741021
op_2['account'] = account # uninvest
9751022

976-
# Create rows
977-
self._add_row(op_1) # sell
978-
self._add_row(op_2) # uninvest
1023+
return self._add_rows(data=[op_1, op_2])
9791024

9801025
def stock_dividend(
9811026
self,
@@ -987,28 +1032,28 @@ def stock_dividend(
9871032
date_order=None,
9881033
notes='',
9891034
):
990-
self._add_row( # stock dividend
991-
{
992-
'date_execution': date_execution,
993-
'operation': 'stock dividend',
994-
'instrument': instrument,
995-
'origin': instrument,
996-
'destination': '',
997-
'price_in': price_in,
998-
'price': 0,
999-
'price_w_expenses': 0,
1000-
'size': size,
1001-
'commission': 0,
1002-
'tax': 0,
1003-
'stated_total': 0,
1004-
'date_order': (date_order if date_order is not None else date_execution),
1005-
'description': f'Stock dividend from {instrument}.',
1006-
'notes': notes,
1007-
'commission_notes': '',
1008-
'tax_notes': '',
1009-
'account': account,
1010-
}
1011-
)
1035+
op_stock_dividend = {
1036+
'date_execution': date_execution,
1037+
'operation': 'stock dividend',
1038+
'instrument': instrument,
1039+
'origin': instrument,
1040+
'destination': '',
1041+
'price_in': price_in,
1042+
'price': 0,
1043+
'price_w_expenses': 0,
1044+
'size': size,
1045+
'commission': 0,
1046+
'tax': 0,
1047+
'stated_total': 0,
1048+
'date_order': (date_order if date_order is not None else date_execution),
1049+
'description': f'Stock dividend from {instrument}.',
1050+
'notes': notes,
1051+
'commission_notes': '',
1052+
'tax_notes': '',
1053+
'account': account,
1054+
}
1055+
1056+
return self._add_rows(op_stock_dividend)
10121057

10131058
def withdraw(
10141059
self,
@@ -1040,28 +1085,28 @@ def withdraw(
10401085
notes : str, optional
10411086
Withdraw notes. By default ''.
10421087
"""
1043-
self._add_row( # Withdraw
1044-
{
1045-
'date_execution': date_execution,
1046-
'operation': 'withdraw',
1047-
'instrument': instrument,
1048-
'origin': instrument,
1049-
'destination': '',
1050-
'price_in': instrument,
1051-
'price': 1,
1052-
'price_w_expenses': 1,
1053-
'size': -amount,
1054-
'commission': 0,
1055-
'tax': 0,
1056-
'stated_total': -amount,
1057-
'date_order': (date_order if date_order is not None else date_execution),
1058-
'description': f'Withdraw {instrument}.',
1059-
'notes': notes,
1060-
'commission_notes': '',
1061-
'tax_notes': '',
1062-
'account': account,
1063-
}
1064-
)
1088+
op_withdraw = {
1089+
'date_execution': date_execution,
1090+
'operation': 'withdraw',
1091+
'instrument': instrument,
1092+
'origin': instrument,
1093+
'destination': '',
1094+
'price_in': instrument,
1095+
'price': 1,
1096+
'price_w_expenses': 1,
1097+
'size': -amount,
1098+
'commission': 0,
1099+
'tax': 0,
1100+
'stated_total': -amount,
1101+
'date_order': (date_order if date_order is not None else date_execution),
1102+
'description': f'Withdraw {instrument}.',
1103+
'notes': notes,
1104+
'commission_notes': '',
1105+
'tax_notes': '',
1106+
'account': account,
1107+
}
1108+
1109+
return self._add_rows(op_withdraw)
10651110

10661111
# *****************
10671112
# MARK: METADATA

0 commit comments

Comments
 (0)