folioflex 1.2.2__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {folioflex-1.2.2/folioflex.egg-info → folioflex-1.4.0}/PKG-INFO +45 -46
- {folioflex-1.2.2 → folioflex-1.4.0}/README.md +4 -6
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/budget/budget.py +207 -78
- folioflex-1.4.0/folioflex/configs/config.yml +179 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/app.py +23 -1
- folioflex-1.4.0/folioflex/dashboard/assets/folioflex_192x192.png +0 -0
- folioflex-1.4.0/folioflex/dashboard/assets/manifest.json +12 -0
- folioflex-1.4.0/folioflex/dashboard/components/dash_formats.py +105 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/components/layouts.py +7 -6
- folioflex-1.4.0/folioflex/dashboard/pages/budget.py +582 -0
- folioflex-1.4.0/folioflex/dashboard/pages/macro.py +181 -0
- folioflex-1.4.0/folioflex/dashboard/pages/personal.py +446 -0
- folioflex-1.4.0/folioflex/dashboard/pages/sectors.py +238 -0
- folioflex-1.4.0/folioflex/dashboard/pages/stocks.py +262 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/utils/dashboard_helper.py +70 -14
- folioflex-1.4.0/folioflex/portfolio/assets.py +275 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/broker.py +40 -22
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/heatmap.py +12 -1
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/helper.py +13 -5
- folioflex-1.4.0/folioflex/portfolio/loans.py +303 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/portfolio.py +178 -53
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/wrappers.py +248 -53
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/utils/cli.py +2 -2
- folioflex-1.4.0/folioflex/utils/config_helper.py +202 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/utils/cq.py +3 -0
- folioflex-1.4.0/folioflex/utils/database.py +254 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/utils/mailer.py +2 -2
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/version.py +1 -1
- {folioflex-1.2.2 → folioflex-1.4.0/folioflex.egg-info}/PKG-INFO +45 -46
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex.egg-info/SOURCES.txt +8 -6
- folioflex-1.4.0/folioflex.egg-info/requires.txt +47 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/pyproject.toml +44 -40
- folioflex-1.4.0/tests/test_asset_loans.py +72 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/tests/test_budget.py +25 -5
- {folioflex-1.2.2 → folioflex-1.4.0}/tests/test_portfolio.py +3 -2
- {folioflex-1.2.2 → folioflex-1.4.0}/tests/test_utils.py +2 -2
- folioflex-1.2.2/folioflex/configs/budget_personal.ini +0 -33
- folioflex-1.2.2/folioflex/configs/config.ini +0 -31
- folioflex-1.2.2/folioflex/configs/portfolio_dash.ini +0 -21
- folioflex-1.2.2/folioflex/configs/portfolio_demo.ini +0 -35
- folioflex-1.2.2/folioflex/configs/portfolio_personal.ini +0 -89
- folioflex-1.2.2/folioflex/configs/transactions_dash.csv +0 -13
- folioflex-1.2.2/folioflex/dashboard/pages/budget.py +0 -194
- folioflex-1.2.2/folioflex/dashboard/pages/macro.py +0 -157
- folioflex-1.2.2/folioflex/dashboard/pages/personal.py +0 -447
- folioflex-1.2.2/folioflex/dashboard/pages/sectors.py +0 -206
- folioflex-1.2.2/folioflex/dashboard/pages/stocks.py +0 -267
- folioflex-1.2.2/folioflex/utils/config_helper.py +0 -206
- folioflex-1.2.2/folioflex.egg-info/requires.txt +0 -46
- {folioflex-1.2.2 → folioflex-1.4.0}/LICENSE.md +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/MANIFEST.in +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/docs/source/conf.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/budget/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/budget/models.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/chatbot/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/chatbot/providers.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/chatbot/scraper.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/configs/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/configs/transactions_demo.csv +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/assets/folioflex_logo.ico +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/assets/folioflex_logo.png +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/assets/github-mark-white.png +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/components/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/components/auth.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/pages/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/pages/ideas.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/pages/login.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/pages/logout.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/dashboard/utils/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/market/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/market/screener.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/portfolio/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/utils/__init__.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex/utils/custom_logger.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex.egg-info/dependency_links.txt +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex.egg-info/entry_points.txt +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/folioflex.egg-info/top_level.txt +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/setup.cfg +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/setup.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/tests/test_chatbot.py +0 -0
- {folioflex-1.2.2 → folioflex-1.4.0}/tests/test_wrappers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: folioflex
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A collection of portfolio tracking capabilities
|
|
5
5
|
Author-email: John Koestner <johnkoestner@outlook.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -27,54 +27,55 @@ License: The MIT License (MIT)
|
|
|
27
27
|
Project-URL: homepage, https://github.com/jkoestner/folioflex
|
|
28
28
|
Project-URL: documentation, https://jkoestner.github.io/folioflex/
|
|
29
29
|
Project-URL: repository, https://github.com/jkoestner/folioflex
|
|
30
|
-
Requires-Python: >=3.
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
License-File: LICENSE.md
|
|
33
|
-
Requires-Dist: ipywidgets>=8.1.
|
|
34
|
-
Requires-Dist: fredapi>=0.
|
|
33
|
+
Requires-Dist: ipywidgets>=8.1.5
|
|
34
|
+
Requires-Dist: fredapi>=0.5.2
|
|
35
35
|
Requires-Dist: jupyter-dash>=0.4.2
|
|
36
|
-
Requires-Dist: jupyterlab>=4.0
|
|
36
|
+
Requires-Dist: jupyterlab>=4.3.0
|
|
37
37
|
Requires-Dist: kaleido<0.2.0,>=0.1.0
|
|
38
|
-
Requires-Dist: numpy>=1.
|
|
39
|
-
Requires-Dist: openpyxl>=3.
|
|
40
|
-
Requires-Dist: pandas>=
|
|
41
|
-
Requires-Dist: pandas-market-calendars>=4.1
|
|
42
|
-
Requires-Dist: pyxirr>=0.
|
|
43
|
-
Requires-Dist: sqlalchemy>=2.0.
|
|
44
|
-
Requires-Dist: yfinance>=0.2.
|
|
38
|
+
Requires-Dist: numpy>=1.26.4
|
|
39
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
40
|
+
Requires-Dist: pandas>=2.2.3
|
|
41
|
+
Requires-Dist: pandas-market-calendars>=4.4.1
|
|
42
|
+
Requires-Dist: pyxirr>=0.10.6
|
|
43
|
+
Requires-Dist: sqlalchemy>=2.0.36
|
|
44
|
+
Requires-Dist: yfinance>=0.2.48
|
|
45
45
|
Requires-Dist: tzlocal>=5.2
|
|
46
|
-
Requires-Dist: dash>=
|
|
46
|
+
Requires-Dist: dash>=2.18.2
|
|
47
|
+
Requires-Dist: dash-ag-grid>=31.2.0
|
|
47
48
|
Requires-Dist: dash_bootstrap_components>=1.6.0
|
|
48
|
-
Requires-Dist: dash-core-components>=
|
|
49
|
-
Requires-Dist: dash-html-components>=
|
|
50
|
-
Requires-Dist: dash-renderer>=1.
|
|
51
|
-
Requires-Dist: dash-table>=
|
|
52
|
-
Requires-Dist: Flask>=
|
|
53
|
-
Requires-Dist: Flask-Compress>=1.
|
|
49
|
+
Requires-Dist: dash-core-components>=2.0.0
|
|
50
|
+
Requires-Dist: dash-html-components>=2.0.0
|
|
51
|
+
Requires-Dist: dash-renderer>=1.9.1
|
|
52
|
+
Requires-Dist: dash-table>=5.0.0
|
|
53
|
+
Requires-Dist: Flask>=3.0.3
|
|
54
|
+
Requires-Dist: Flask-Compress>=1.17
|
|
54
55
|
Requires-Dist: Flask-Login>=0.6.3
|
|
55
|
-
Requires-Dist: gunicorn>=
|
|
56
|
-
Requires-Dist: celery>=5.
|
|
57
|
-
Requires-Dist: flower>=2.0.
|
|
58
|
-
Requires-Dist: redis>=
|
|
59
|
-
Requires-Dist: g4f
|
|
60
|
-
Requires-Dist: hugchat>=0.
|
|
61
|
-
Requires-Dist: openai>=1.3
|
|
62
|
-
Requires-Dist: pyautogui>=0.9.
|
|
63
|
-
Requires-Dist: seleniumbase>=4.
|
|
64
|
-
Requires-Dist: emoji>=2.
|
|
65
|
-
Requires-Dist: gensim>=4.3.
|
|
66
|
-
Requires-Dist: scikit-learn>=1.
|
|
67
|
-
Requires-Dist: scipy
|
|
68
|
-
Requires-Dist: psycopg2-binary
|
|
56
|
+
Requires-Dist: gunicorn>=23.0.0
|
|
57
|
+
Requires-Dist: celery>=5.4.0
|
|
58
|
+
Requires-Dist: flower>=2.0.1
|
|
59
|
+
Requires-Dist: redis>=5.2.0
|
|
60
|
+
Requires-Dist: g4f>=0.3.3.3
|
|
61
|
+
Requires-Dist: hugchat>=0.4.12
|
|
62
|
+
Requires-Dist: openai>=1.54.3
|
|
63
|
+
Requires-Dist: pyautogui>=0.9.54
|
|
64
|
+
Requires-Dist: seleniumbase>=4.32.8
|
|
65
|
+
Requires-Dist: emoji>=2.14.0
|
|
66
|
+
Requires-Dist: gensim>=4.3.3
|
|
67
|
+
Requires-Dist: scikit-learn>=1.5.2
|
|
68
|
+
Requires-Dist: scipy<1.14
|
|
69
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
|
69
70
|
Provides-Extra: dev
|
|
70
|
-
Requires-Dist: black>=
|
|
71
|
+
Requires-Dist: black>=24.10.0; extra == "dev"
|
|
71
72
|
Requires-Dist: isort>=5.13.2; extra == "dev"
|
|
72
|
-
Requires-Dist: jupyterlab-code-formatter>=
|
|
73
|
-
Requires-Dist: pytest>=
|
|
74
|
-
Requires-Dist: pytest-cov>=
|
|
75
|
-
Requires-Dist: ruff>=0.
|
|
76
|
-
Requires-Dist: sphinx
|
|
77
|
-
Requires-Dist: sphinx_rtd_theme>=
|
|
73
|
+
Requires-Dist: jupyterlab-code-formatter>=3.0.2; extra == "dev"
|
|
74
|
+
Requires-Dist: pytest>=8.3.3; extra == "dev"
|
|
75
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
76
|
+
Requires-Dist: ruff>=0.7.3; extra == "dev"
|
|
77
|
+
Requires-Dist: sphinx>=8.1; extra == "dev"
|
|
78
|
+
Requires-Dist: sphinx_rtd_theme>=3.0.1; extra == "dev"
|
|
78
79
|
|
|
79
80
|
<div align="center">
|
|
80
81
|
<img src="https://user-images.githubusercontent.com/50647092/260895211-7e9a8e6e-9f85-48ed-bb61-49ff2ff805b6.png"><br>
|
|
@@ -118,13 +119,11 @@ Simple investment portfolio tool that will track stock and provide returns and o
|
|
|
118
119
|
**🔧 Features:**
|
|
119
120
|
|
|
120
121
|
- **Market Screener**: Filter and find trending stocks. 🔍
|
|
121
|
-

|
|
122
|
+

|
|
123
123
|
- **Portfolio Management**: Organize and track, your investments. 💼
|
|
124
|
-

|
|
125
125
|
- **Budget Tool**: Create and monitor a budget. 💰
|
|
126
|
-

|
|
126
|
+

|
|
128
127
|
|
|
129
128
|
**📚 Documentation:**
|
|
130
129
|
|
|
@@ -244,7 +243,7 @@ When using the portfolio class, the following code can be used to get the return
|
|
|
244
243
|
|
|
245
244
|
```python
|
|
246
245
|
from folioflex.portfolio.portfolio import Portfolio
|
|
247
|
-
config_path = "portfolio_demo.
|
|
246
|
+
config_path = "portfolio_demo.yml"
|
|
248
247
|
pf = Portfolio(
|
|
249
248
|
config_path=config_path,
|
|
250
249
|
portfolio='company_a'
|
|
@@ -40,13 +40,11 @@ Simple investment portfolio tool that will track stock and provide returns and o
|
|
|
40
40
|
**🔧 Features:**
|
|
41
41
|
|
|
42
42
|
- **Market Screener**: Filter and find trending stocks. 🔍
|
|
43
|
-

|
|
43
|
+

|
|
45
44
|
- **Portfolio Management**: Organize and track, your investments. 💼
|
|
46
|
-

|
|
47
46
|
- **Budget Tool**: Create and monitor a budget. 💰
|
|
48
|
-

|
|
47
|
+

|
|
50
48
|
|
|
51
49
|
**📚 Documentation:**
|
|
52
50
|
|
|
@@ -166,7 +164,7 @@ When using the portfolio class, the following code can be used to get the return
|
|
|
166
164
|
|
|
167
165
|
```python
|
|
168
166
|
from folioflex.portfolio.portfolio import Portfolio
|
|
169
|
-
config_path = "portfolio_demo.
|
|
167
|
+
config_path = "portfolio_demo.yml"
|
|
170
168
|
pf = Portfolio(
|
|
171
169
|
config_path=config_path,
|
|
172
170
|
portfolio='company_a'
|
|
@@ -13,14 +13,14 @@ Some methods that are included are:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
|
+
from typing import Any, Dict, List, Optional, Union
|
|
16
17
|
|
|
17
18
|
import emoji
|
|
18
19
|
import numpy as np
|
|
19
20
|
import pandas as pd
|
|
20
21
|
import plotly.express as px
|
|
21
|
-
import sqlalchemy as sa
|
|
22
22
|
|
|
23
|
-
from folioflex.utils import config_helper, custom_logger
|
|
23
|
+
from folioflex.utils import config_helper, custom_logger, database
|
|
24
24
|
|
|
25
25
|
logger = custom_logger.setup_logging(__name__)
|
|
26
26
|
|
|
@@ -40,16 +40,23 @@ class Budget:
|
|
|
40
40
|
|
|
41
41
|
def __init__(
|
|
42
42
|
self,
|
|
43
|
-
config_path,
|
|
44
|
-
budget,
|
|
45
|
-
):
|
|
43
|
+
config_path: str,
|
|
44
|
+
budget: str,
|
|
45
|
+
) -> None:
|
|
46
46
|
"""Initialize the Portfolio class."""
|
|
47
47
|
logger.info("Initializing Budget class.")
|
|
48
|
-
self.
|
|
48
|
+
self.config_path = config_path
|
|
49
|
+
self.config_dict = config_helper.get_config_options(
|
|
50
|
+
config_path, "budgets", budget
|
|
51
|
+
)
|
|
49
52
|
self.budgets = self.config_dict["budgets"]
|
|
50
53
|
self.default = self.config_dict["default"]
|
|
54
|
+
self.budget = budget
|
|
55
|
+
self.model = self.config_dict.get("model", None)
|
|
51
56
|
|
|
52
|
-
def get_transactions(
|
|
57
|
+
def get_transactions(
|
|
58
|
+
self, engine: Optional[Any] = None, user: Optional[str] = None
|
|
59
|
+
) -> pd.DataFrame:
|
|
53
60
|
"""
|
|
54
61
|
Get the transactions for the budget.
|
|
55
62
|
|
|
@@ -59,6 +66,8 @@ class Budget:
|
|
|
59
66
|
----------
|
|
60
67
|
engine : SQLAlchemy engine
|
|
61
68
|
The engine to connect to the database.
|
|
69
|
+
user : str, optional
|
|
70
|
+
The user to get the transactions for.
|
|
62
71
|
|
|
63
72
|
Returns
|
|
64
73
|
-------
|
|
@@ -79,16 +88,16 @@ class Budget:
|
|
|
79
88
|
|
|
80
89
|
"""
|
|
81
90
|
if engine is None:
|
|
82
|
-
engine = self.
|
|
91
|
+
engine = database.Engine(self.config_path)
|
|
92
|
+
if user is None:
|
|
93
|
+
user = self.config_dict.get("user", None)
|
|
83
94
|
logger.info("Getting transactions.")
|
|
84
95
|
# creating a dataframe of transactions that include
|
|
85
96
|
# the account name, item name, and transaction name
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
with engine.connect() as conn, conn.begin():
|
|
91
|
-
account_df = pd.read_sql_table("accounts_table", conn)
|
|
97
|
+
user_df = engine.read_table("users_table")
|
|
98
|
+
item_df = engine.read_table("items_table")
|
|
99
|
+
account_df = engine.read_table("accounts_table")
|
|
100
|
+
tx_df = engine.read_table("transactions_table")
|
|
92
101
|
# creating a grouped dataset
|
|
93
102
|
tx_df = pd.merge(
|
|
94
103
|
tx_df,
|
|
@@ -100,12 +109,22 @@ class Budget:
|
|
|
100
109
|
)
|
|
101
110
|
tx_df = pd.merge(
|
|
102
111
|
tx_df,
|
|
103
|
-
item_df[["id", "plaid_institution_id"]],
|
|
112
|
+
item_df[["id", "plaid_institution_id", "user_id"]],
|
|
104
113
|
left_on="item_id",
|
|
105
114
|
right_on="id",
|
|
106
115
|
how="left",
|
|
107
116
|
suffixes=[None, "_tmp"],
|
|
108
117
|
)
|
|
118
|
+
tx_df = pd.merge(
|
|
119
|
+
tx_df,
|
|
120
|
+
user_df[["id", "username"]],
|
|
121
|
+
left_on="user_id",
|
|
122
|
+
right_on="id",
|
|
123
|
+
how="left",
|
|
124
|
+
suffixes=[None, "_tmp"],
|
|
125
|
+
)
|
|
126
|
+
if user is not None:
|
|
127
|
+
tx_df = tx_df[tx_df["username"] == user]
|
|
109
128
|
tx_df = tx_df[
|
|
110
129
|
[
|
|
111
130
|
"id",
|
|
@@ -136,6 +155,12 @@ class Budget:
|
|
|
136
155
|
"""
|
|
137
156
|
Modify the transactions for the budget.
|
|
138
157
|
|
|
158
|
+
The modifications include:
|
|
159
|
+
- Zero out the amount for the given columns
|
|
160
|
+
- Extract the text between quotes
|
|
161
|
+
- Preprocess the emoji in the text
|
|
162
|
+
- Remove the pending transactions.
|
|
163
|
+
|
|
139
164
|
Parameters
|
|
140
165
|
----------
|
|
141
166
|
tx_df : DataFrame
|
|
@@ -285,6 +310,74 @@ class Budget:
|
|
|
285
310
|
return "Incoming Transfer"
|
|
286
311
|
return "OTHER"
|
|
287
312
|
|
|
313
|
+
def identify_subscriptions(self, tx_df):
|
|
314
|
+
"""
|
|
315
|
+
Identify possible subscriptions in the transactions.
|
|
316
|
+
|
|
317
|
+
The subscriptions are identified by the name and also the amount. If
|
|
318
|
+
the amount is similar for multiple transactions, and there are at least
|
|
319
|
+
a set amount of transcations, then it is likely a subscription.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
tx_df : DataFrame
|
|
324
|
+
The transactions to identify subscriptions for.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
subscriptions : DataFrame
|
|
329
|
+
The subscriptions identified in the transactions.
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
# default values
|
|
333
|
+
min_transactions = 3
|
|
334
|
+
interval_std_threshold = 5
|
|
335
|
+
amount_std_threshold = 0.1
|
|
336
|
+
|
|
337
|
+
# group data by name
|
|
338
|
+
grouped_df = tx_df.groupby("name")
|
|
339
|
+
|
|
340
|
+
# identify subscriptions
|
|
341
|
+
subscriptions = []
|
|
342
|
+
for name, group in grouped_df:
|
|
343
|
+
if len(group) < min_transactions:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# calculating the intervals and ensure they are regular
|
|
347
|
+
intervals = group["date"].diff().dropna().dt.days
|
|
348
|
+
regular = intervals.std() <= interval_std_threshold
|
|
349
|
+
|
|
350
|
+
# calculate the amount and ensure it is consistent
|
|
351
|
+
amount_mean = group["amount"].mean()
|
|
352
|
+
if amount_mean == 0:
|
|
353
|
+
continue
|
|
354
|
+
relative_std = group["amount"].std() / amount_mean
|
|
355
|
+
consistent_amount = relative_std <= amount_std_threshold
|
|
356
|
+
|
|
357
|
+
# get the last date and amount
|
|
358
|
+
last_date = group["date"].max()
|
|
359
|
+
last_amount = group[group["date"] == last_date]["amount"].values[0]
|
|
360
|
+
|
|
361
|
+
if regular and consistent_amount:
|
|
362
|
+
subscriptions.append(
|
|
363
|
+
{
|
|
364
|
+
"Description": name,
|
|
365
|
+
"Occurrences": len(group),
|
|
366
|
+
"Mean Interval (Days)": intervals.mean(),
|
|
367
|
+
"Amount Mean": group["amount"].mean(),
|
|
368
|
+
"Amount Std Dev": group["amount"].std(),
|
|
369
|
+
"Last Date": last_date,
|
|
370
|
+
"Last Amount": last_amount,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
subscriptions_df = pd.DataFrame(subscriptions)
|
|
375
|
+
subscriptions_df = subscriptions_df.sort_values(
|
|
376
|
+
by="Occurrences", ascending=False
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return subscriptions_df
|
|
380
|
+
|
|
288
381
|
def budget_view(self, tx_df, target_date, exclude_labels=None):
|
|
289
382
|
"""
|
|
290
383
|
Provide a view of categories and their budget status.
|
|
@@ -308,6 +401,7 @@ class Budget:
|
|
|
308
401
|
if exclude_labels is not None:
|
|
309
402
|
tx_df = tx_df[~tx_df["label"].isin(exclude_labels)]
|
|
310
403
|
budgets = {k: v for k, v in budgets.items() if k not in exclude_labels}
|
|
404
|
+
|
|
311
405
|
# grouping transactions
|
|
312
406
|
grouped_transactions = (
|
|
313
407
|
tx_df[tx_df["date"].dt.to_period("M") == target_date]
|
|
@@ -316,6 +410,7 @@ class Budget:
|
|
|
316
410
|
.reset_index()
|
|
317
411
|
.sort_values("amount", ascending=True)
|
|
318
412
|
)
|
|
413
|
+
|
|
319
414
|
# adding in the budget
|
|
320
415
|
unused_keys = set(budgets.keys()) - set(grouped_transactions["label"])
|
|
321
416
|
unused_keys_df = pd.DataFrame({"label": list(unused_keys)})
|
|
@@ -327,6 +422,7 @@ class Budget:
|
|
|
327
422
|
grouped_transactions["budget"] = grouped_transactions["budget"].fillna(
|
|
328
423
|
float(self.config_dict["default"])
|
|
329
424
|
)
|
|
425
|
+
|
|
330
426
|
# providing a total
|
|
331
427
|
totals = grouped_transactions.sum(numeric_only=True).to_frame().T
|
|
332
428
|
totals["label"] = "TOTAL"
|
|
@@ -517,7 +613,81 @@ class Budget:
|
|
|
517
613
|
|
|
518
614
|
return fig
|
|
519
615
|
|
|
520
|
-
def
|
|
616
|
+
def display_category_trend(self, tx_df, category):
|
|
617
|
+
"""
|
|
618
|
+
Display the category trend view as a bar chart.
|
|
619
|
+
|
|
620
|
+
Parameters
|
|
621
|
+
----------
|
|
622
|
+
tx_df : DataFrame
|
|
623
|
+
The transactions to view income status for.
|
|
624
|
+
category : str
|
|
625
|
+
The category to view the trend for.
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
fig : Figure
|
|
630
|
+
The category trend view as a line chart.
|
|
631
|
+
|
|
632
|
+
"""
|
|
633
|
+
# filter data for category
|
|
634
|
+
category_df = tx_df[tx_df["label"] == category]
|
|
635
|
+
category_df = category_df.groupby(["month"])["amount"].sum().reset_index()
|
|
636
|
+
if len(category_df) == 0:
|
|
637
|
+
logger.warning(f"No transactions found for category: {category}")
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
# budgets
|
|
641
|
+
category_budget = self.budgets.get(category, 0)
|
|
642
|
+
average_budget = category_df["amount"].mean()
|
|
643
|
+
twelve_month_avg = category_df["amount"].rolling(window=12).mean()
|
|
644
|
+
|
|
645
|
+
# create bar chart
|
|
646
|
+
fig = px.bar(
|
|
647
|
+
category_df,
|
|
648
|
+
x="month",
|
|
649
|
+
y="amount",
|
|
650
|
+
title=f"'{category}' Spending Trend",
|
|
651
|
+
labels={"month": "Month", "amount": "Amount"},
|
|
652
|
+
color_discrete_sequence=["blue"],
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# add budget line
|
|
656
|
+
fig.add_scatter(
|
|
657
|
+
x=category_df["month"],
|
|
658
|
+
y=[category_budget] * len(category_df),
|
|
659
|
+
mode="lines",
|
|
660
|
+
line={"color": "red", "dash": "dash"},
|
|
661
|
+
name="Budget",
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# add average line
|
|
665
|
+
fig.add_scatter(
|
|
666
|
+
x=category_df["month"],
|
|
667
|
+
y=[average_budget] * len(category_df),
|
|
668
|
+
mode="lines",
|
|
669
|
+
line={"color": "black", "dash": "dash"},
|
|
670
|
+
name="Average",
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# add 12 month average line
|
|
674
|
+
fig.add_scatter(
|
|
675
|
+
x=category_df["month"],
|
|
676
|
+
y=twelve_month_avg,
|
|
677
|
+
mode="lines",
|
|
678
|
+
line={"color": "green", "dash": "dash"},
|
|
679
|
+
name="12-Month Average",
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return fig
|
|
683
|
+
|
|
684
|
+
def category_tx_view(
|
|
685
|
+
self,
|
|
686
|
+
tx_df,
|
|
687
|
+
target_date,
|
|
688
|
+
category,
|
|
689
|
+
columns=None,
|
|
690
|
+
):
|
|
521
691
|
"""
|
|
522
692
|
Display the transactions view as a table.
|
|
523
693
|
|
|
@@ -529,6 +699,8 @@ class Budget:
|
|
|
529
699
|
The date to view transactions for. Format: YYYY-MM.
|
|
530
700
|
category : str
|
|
531
701
|
The category to view transactions for.
|
|
702
|
+
columns : list (optional)
|
|
703
|
+
The columns to view in the table.
|
|
532
704
|
|
|
533
705
|
Returns
|
|
534
706
|
-------
|
|
@@ -536,10 +708,20 @@ class Budget:
|
|
|
536
708
|
The category of transactions view as a table.
|
|
537
709
|
|
|
538
710
|
"""
|
|
711
|
+
if columns is None:
|
|
712
|
+
columns = [
|
|
713
|
+
"date",
|
|
714
|
+
"amount",
|
|
715
|
+
"name",
|
|
716
|
+
"label",
|
|
717
|
+
"official_name",
|
|
718
|
+
]
|
|
719
|
+
|
|
539
720
|
cat_tx_df = tx_df[
|
|
540
721
|
(tx_df["date"].dt.to_period("M") == target_date)
|
|
541
722
|
& (tx_df["label"].str.contains(category))
|
|
542
|
-
].sort_values(by="
|
|
723
|
+
].sort_values(by="date", ascending=False)
|
|
724
|
+
cat_tx_df = cat_tx_df[columns]
|
|
543
725
|
return cat_tx_df
|
|
544
726
|
|
|
545
727
|
def update_labels_db(self, tx_df, engine=None, label_column="label"):
|
|
@@ -561,68 +743,15 @@ class Budget:
|
|
|
561
743
|
|
|
562
744
|
"""
|
|
563
745
|
if engine is None:
|
|
564
|
-
engine = self.
|
|
746
|
+
engine = database.Engine(self.config_path)
|
|
565
747
|
logger.info("Updating labels in database.")
|
|
566
|
-
rows_updated = 0
|
|
567
|
-
with engine.connect() as conn:
|
|
568
|
-
for _, row in tx_df.iterrows():
|
|
569
|
-
if pd.isna(row[label_column]):
|
|
570
|
-
update_label_query = sa.text(
|
|
571
|
-
"""
|
|
572
|
-
UPDATE public.transactions_table
|
|
573
|
-
SET "label" = NULL
|
|
574
|
-
WHERE id = :id;
|
|
575
|
-
"""
|
|
576
|
-
)
|
|
577
|
-
executed = conn.execute(update_label_query, {"id": row["id"]})
|
|
578
|
-
else:
|
|
579
|
-
update_label_query = sa.text(
|
|
580
|
-
"""
|
|
581
|
-
UPDATE public.transactions_table
|
|
582
|
-
SET "label" = :label
|
|
583
|
-
WHERE id = :id;
|
|
584
|
-
"""
|
|
585
|
-
)
|
|
586
|
-
executed = conn.execute(
|
|
587
|
-
update_label_query,
|
|
588
|
-
{"label": row[label_column], "id": row["id"]},
|
|
589
|
-
)
|
|
590
|
-
rows_updated = rows_updated + executed.rowcount
|
|
591
|
-
conn.commit()
|
|
592
|
-
logger.info(f"Rows updated: {rows_updated}")
|
|
593
|
-
|
|
594
|
-
def _create_engine(self):
|
|
595
|
-
"""
|
|
596
|
-
Create an engine.
|
|
597
748
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
engine : SQLAlchemy engine
|
|
601
|
-
The engine to connect to the database.
|
|
749
|
+
# filter the table
|
|
750
|
+
tx_df = tx_df[[label_column, "id"]]
|
|
602
751
|
|
|
603
|
-
""
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
db_host = self.config_dict.get("db_host", None)
|
|
607
|
-
db_name = self.config_dict.get("db_name", None)
|
|
608
|
-
db_port = self.config_dict.get("db_port", None)
|
|
609
|
-
|
|
610
|
-
config_values = {
|
|
611
|
-
"db_user": db_user,
|
|
612
|
-
"db_pass": db_pass,
|
|
613
|
-
"db_host": db_host,
|
|
614
|
-
"db_name": db_name,
|
|
615
|
-
"db_port": db_port,
|
|
616
|
-
}
|
|
617
|
-
missing_values = [key for key, value in config_values.items() if value is None]
|
|
618
|
-
if missing_values:
|
|
619
|
-
raise ValueError(
|
|
620
|
-
f"The following configuration values are missing: "
|
|
621
|
-
f"{', '.join(missing_values)}"
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
logger.info("Connecting to database.")
|
|
625
|
-
engine = sa.create_engine(
|
|
626
|
-
f"postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
|
|
752
|
+
tx_df = tx_df.rename(columns={label_column: "label"})
|
|
753
|
+
engine.bulk_update(
|
|
754
|
+
tx_df=tx_df, table_name="transactions_table", where_column="id"
|
|
627
755
|
)
|
|
628
|
-
|
|
756
|
+
|
|
757
|
+
logger.info(f"Rows updated: {len(tx_df)}")
|