Skip to content

Commit 4ec49a9

Browse files
authored
Implement a basic, but working, version (#2)
1 parent 78fd364 commit 4ec49a9

14 files changed

+528
-56
lines changed

.devcontainer/Dockerfile.dev

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ LABEL author="NCR"
33

44
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
55

6+
# Uninstall pre-installed formatting and linting tools
7+
# They would conflict with our pinned versions
8+
RUN \
9+
pipx uninstall black \
10+
&& pipx uninstall pydocstyle \
11+
&& pipx uninstall pycodestyle \
12+
&& pipx uninstall mypy \
13+
&& pipx uninstall pylint
14+
615
ENV FILE_LOCATION "/usr/src/app"
716

817
# Disable host checking

.devcontainer/devcontainer.json

+58-53
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,62 @@
11
{
2-
"name": "Pypmanager",
3-
"context": "..",
4-
"dockerFile": "Dockerfile.dev",
5-
"containerUser": "root",
6-
"remoteUser": "root",
7-
"postCreateCommand": "script/setup.sh",
8-
"containerEnv": {
9-
"DEVCONTAINER": "1"
10-
},
11-
"customizations": {
12-
"vscode": {
13-
"settings": {
14-
"python.pythonPath": "/usr/local/bin/python",
15-
"python.linting.enabled": true,
16-
"python.linting.pylintEnabled": true,
17-
"python.formatting.blackPath": "/usr/local/bin/black",
18-
"python.linting.mypyPath": "/usr/local/bin/mypy",
19-
"python.linting.pylintPath": "/usr/local/bin/pylint",
20-
"python.formatting.provider": "black",
21-
"editor.formatOnPaste": false,
22-
"editor.formatOnSave": true,
23-
"editor.formatOnType": true,
24-
"files.trimTrailingWhitespace": true,
25-
"terminal.integrated.profiles.linux": {
26-
"zsh": {
27-
"path": "/usr/bin/zsh"
28-
}
29-
},
30-
"terminal.integrated.defaultProfile.linux": "zsh",
31-
"python.terminal.activateEnvInCurrentTerminal": true,
32-
"python.analysis.extraPaths": [
33-
"/workspaces/pypmanager"
34-
],
35-
"[python]": {
36-
"editor.codeActionsOnSave": {
37-
"source.organizeImports": true
38-
}
39-
},
40-
"python.formatting.blackArgs": [
41-
"-S",
42-
"--line-length",
43-
"88"
44-
],
45-
"python.testing.pytestEnabled": true,
46-
"python.testing.unittestArgs": [
47-
"-no-cov",
48-
"-s"
49-
]
2+
"name": "Pypmanager",
3+
"context": "..",
4+
"dockerFile": "Dockerfile.dev",
5+
"containerUser": "root",
6+
"remoteUser": "root",
7+
"runArgs": [
8+
"-e",
9+
"GIT_EDITOR=code --wait"
10+
],
11+
"postCreateCommand": "script/setup.sh",
12+
"containerEnv": {
13+
"DEVCONTAINER": "1"
14+
},
15+
"customizations": {
16+
"vscode": {
17+
"settings": {
18+
"python.pythonPath": "/usr/local/bin/python",
19+
"python.linting.enabled": true,
20+
"python.linting.pylintEnabled": true,
21+
"python.formatting.blackPath": "/usr/local/bin/black",
22+
"python.linting.mypyPath": "/usr/local/bin/mypy",
23+
"python.linting.pylintPath": "/usr/local/bin/pylint",
24+
"python.formatting.provider": "black",
25+
"editor.formatOnPaste": false,
26+
"editor.formatOnSave": true,
27+
"editor.formatOnType": true,
28+
"files.trimTrailingWhitespace": true,
29+
"terminal.integrated.profiles.linux": {
30+
"zsh": {
31+
"path": "/usr/bin/zsh"
32+
}
5033
},
51-
"extensions": [
52-
"ms-python.python",
53-
"ms-python.vscode-pylance"
34+
"terminal.integrated.defaultProfile.linux": "zsh",
35+
"python.terminal.activateEnvInCurrentTerminal": true,
36+
"python.analysis.extraPaths": [
37+
"/workspaces/pypmanager"
38+
],
39+
"[python]": {
40+
"editor.codeActionsOnSave": {
41+
"source.organizeImports": true
42+
}
43+
},
44+
"python.formatting.blackArgs": [
45+
"-S",
46+
"--line-length",
47+
"88"
48+
],
49+
"python.testing.pytestEnabled": true,
50+
"python.testing.unittestArgs": [
51+
"-no-cov",
52+
"-s"
5453
]
55-
}
54+
},
55+
"extensions": [
56+
"ms-python.python",
57+
"ms-python.vscode-pylance",
58+
"mechatroner.rainbow-csv"
59+
]
5660
}
57-
}
61+
}
62+
}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,5 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
*.csv

pypmanager/data_loader.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Data loaders."""
2+
from abc import abstractmethod
3+
from enum import StrEnum
4+
5+
import pandas as pd
6+
7+
NORMALISED_COL_NAMES_AVANZA = {
8+
"Datum": "transaction_date",
9+
"Konto": "account",
10+
"Typ av transaktion": "transaction_type",
11+
"Värdepapper/beskrivning": "name",
12+
"Antal": "no_traded",
13+
"Kurs": "price",
14+
"Belopp": "amount",
15+
"Courtage": "commission",
16+
"Valuta": "currency",
17+
"ISIN": "isin_code",
18+
"Resultat": "pnl",
19+
}
20+
21+
NORMALISED_COL_NAMES_LYSA = {
22+
"Date": "transaction_date",
23+
"Type": "transaction_type",
24+
"Amount": "amount",
25+
"Counterpart/Fund": "name",
26+
"Volume": "no_traded",
27+
"Price": "price",
28+
}
29+
30+
DTYPES_MAP = {
31+
"account": str,
32+
"transaction_type": str,
33+
"name": str,
34+
"no_traded": float,
35+
"price": float,
36+
"amount": float,
37+
"commission": float,
38+
"currency": str,
39+
"isin_code": str,
40+
"pnl": float,
41+
}
42+
43+
44+
class TransactionTypeValues(StrEnum):
45+
"""Represent transaction types."""
46+
47+
BUY = "buy"
48+
SELL = "sell"
49+
50+
51+
class DataLoader:
52+
"""Base data loader."""
53+
54+
df: pd.DataFrame | None = None
55+
56+
def __init__(self) -> None:
57+
"""Init class."""
58+
self.load_csv()
59+
self.filter_transactions()
60+
self.ensure_dot()
61+
self.cleanup_df()
62+
self.convert_data_types()
63+
self.finalize_data_load()
64+
65+
@abstractmethod
66+
def load_csv(self) -> None:
67+
"""Load CSV."""
68+
69+
def filter_transactions(self) -> None:
70+
"""Filter transactions."""
71+
self.df = self.df.query(
72+
f"transaction_type == '{TransactionTypeValues.BUY}' or "
73+
f"transaction_type == '{TransactionTypeValues.SELL}'"
74+
)
75+
76+
def ensure_dot(self) -> None:
77+
"""Make sure values have dot as decimal separator."""
78+
df = self.df.copy()
79+
80+
for col in ("no_traded", "price", "amount", "commission", "pnl"):
81+
if col in df.columns:
82+
df[col].replace(",", ".", regex=True, inplace=True)
83+
84+
self.df = df
85+
86+
def convert_data_types(self) -> None:
87+
"""Convert data types."""
88+
df = self.df.copy()
89+
for key, val in DTYPES_MAP.items():
90+
if key in df.columns:
91+
df[key] = df[key].astype(val)
92+
93+
self.df = df
94+
95+
def cleanup_df(self) -> None:
96+
"""Cleanup dataframe."""
97+
df = self.df.copy()
98+
99+
for col in ("commission", "pnl", "isin_code"): # Replace dashes with 0
100+
if col in df.columns:
101+
try:
102+
df[col] = df[col].str.replace("-", "").replace("", 0)
103+
except AttributeError:
104+
pass
105+
106+
self.df = df
107+
108+
def finalize_data_load(self) -> None:
109+
"""Post-process."""
110+
df = self.df.copy()
111+
df["amount"] = abs(df["amount"])
112+
113+
self.df = df
114+
115+
116+
class LysaLoader(DataLoader):
117+
"""Data loader for Lysa."""
118+
119+
def load_csv(self) -> None:
120+
"""Load CSV."""
121+
files = ["data/lysa-a.csv", "data/lysa-b.csv"]
122+
dfs = [pd.read_csv(file, sep=",") for file in files]
123+
df = pd.concat(dfs, ignore_index=True)
124+
125+
df = df.rename(columns=NORMALISED_COL_NAMES_LYSA)
126+
df.set_index("transaction_date", inplace=True)
127+
128+
df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
129+
130+
# Replace buy
131+
for event in ("Switch buy", "Buy"):
132+
df["transaction_type"] = df["transaction_type"].replace(
133+
event, TransactionTypeValues.BUY.value
134+
)
135+
136+
# Replace sell
137+
for event in ("Switch sell", "Sell"):
138+
df["transaction_type"] = df["transaction_type"].replace(
139+
event, TransactionTypeValues.SELL.value
140+
)
141+
142+
df["commission"] = 0.0
143+
144+
self.df = df
145+
146+
147+
class AvanzaLoader(DataLoader):
148+
"""Data loader for Avanza."""
149+
150+
def load_csv(self) -> None:
151+
"""Load CSV."""
152+
df = pd.read_csv("data/avanza.csv", sep=";")
153+
df = df.rename(columns=NORMALISED_COL_NAMES_AVANZA)
154+
df.set_index("transaction_date", inplace=True)
155+
156+
# Replace buy
157+
for event in ("Köp",):
158+
df["transaction_type"] = df["transaction_type"].replace(
159+
event, TransactionTypeValues.BUY.value
160+
)
161+
162+
# Replace sell
163+
for event in ("Sälj",):
164+
df["transaction_type"] = df["transaction_type"].replace(
165+
event, TransactionTypeValues.SELL.value
166+
)
167+
168+
self.df = df
169+
170+
171+
class MiscLoader(DataLoader):
172+
"""Data loader for misc data."""
173+
174+
def load_csv(self) -> None:
175+
"""Load CSV."""
176+
df = pd.read_csv("data/other-a.csv", sep=";")
177+
df.set_index("transaction_date", inplace=True)
178+
179+
for field in ["commission", "amount"]:
180+
df[field] = df[field].str.replace(",", "")
181+
182+
self.df = df

pypmanager/market_price.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Market price."""
2+
import pandas as pd
3+
4+
5+
class MarketPrice:
6+
"""Represent a security's market price."""
7+
8+
def __init__(self) -> None:
9+
"""Init class."""
10+
df = pd.read_csv("data/market_data.csv", sep=";")
11+
output_dict: dict[str, float] = {}
12+
for _, row in df.iterrows():
13+
output_dict[row.loc['isin_code']] = row.loc['price']
14+
15+
self.lookup_table = output_dict

pypmanager/portfolio.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Handle portfolios."""
2+
3+
4+
from dataclasses import dataclass
5+
6+
from pypmanager.security import Security
7+
8+
9+
@dataclass
10+
class Portfolio:
11+
"""Calculate portfolio value."""
12+
13+
securities: list[Security]
14+
15+
@property
16+
def mtm(self) -> float:
17+
"""Return total market value."""
18+
return sum(
19+
s.market_value for s in self.securities if s.market_value is not None
20+
)
21+
22+
@property
23+
def invested_amount(self) -> float:
24+
"""Return invested amount."""
25+
return sum(
26+
s.invested_amount for s in self.securities if s.invested_amount is not None
27+
)
28+
29+
@property
30+
def realized_pnl(self) -> float:
31+
"""Return realized PnL."""
32+
return sum(
33+
s.realized_pnl for s in self.securities if s.realized_pnl is not None
34+
)
35+
36+
@property
37+
def unrealized_pnl(self) -> float:
38+
"""Return unrealized PnL."""
39+
return sum(
40+
s.unrealized_pnl for s in self.securities if s.unrealized_pnl is not None
41+
)

0 commit comments

Comments
 (0)