folioflex 1.2.2__tar.gz → 1.2.3__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.
Files changed (70) hide show
  1. {folioflex-1.2.2/folioflex.egg-info → folioflex-1.2.3}/PKG-INFO +2 -1
  2. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/budget/budget.py +162 -2
  3. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/portfolio_personal.ini +12 -0
  4. folioflex-1.2.3/folioflex/dashboard/components/dash_formats.py +105 -0
  5. folioflex-1.2.3/folioflex/dashboard/pages/budget.py +386 -0
  6. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/broker.py +1 -1
  7. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/portfolio.py +33 -2
  8. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/version.py +1 -1
  9. {folioflex-1.2.2 → folioflex-1.2.3/folioflex.egg-info}/PKG-INFO +2 -1
  10. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex.egg-info/SOURCES.txt +1 -0
  11. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex.egg-info/requires.txt +1 -0
  12. {folioflex-1.2.2 → folioflex-1.2.3}/pyproject.toml +1 -0
  13. {folioflex-1.2.2 → folioflex-1.2.3}/tests/test_budget.py +24 -4
  14. folioflex-1.2.2/folioflex/dashboard/pages/budget.py +0 -194
  15. {folioflex-1.2.2 → folioflex-1.2.3}/LICENSE.md +0 -0
  16. {folioflex-1.2.2 → folioflex-1.2.3}/MANIFEST.in +0 -0
  17. {folioflex-1.2.2 → folioflex-1.2.3}/README.md +0 -0
  18. {folioflex-1.2.2 → folioflex-1.2.3}/docs/source/conf.py +0 -0
  19. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/__init__.py +0 -0
  20. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/budget/__init__.py +0 -0
  21. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/budget/models.py +0 -0
  22. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/chatbot/__init__.py +0 -0
  23. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/chatbot/providers.py +0 -0
  24. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/chatbot/scraper.py +0 -0
  25. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/__init__.py +0 -0
  26. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/budget_personal.ini +0 -0
  27. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/config.ini +0 -0
  28. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/portfolio_dash.ini +0 -0
  29. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/portfolio_demo.ini +0 -0
  30. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/transactions_dash.csv +0 -0
  31. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/configs/transactions_demo.csv +0 -0
  32. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/__init__.py +0 -0
  33. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/app.py +0 -0
  34. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/assets/folioflex_logo.ico +0 -0
  35. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/assets/folioflex_logo.png +0 -0
  36. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/assets/github-mark-white.png +0 -0
  37. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/components/__init__.py +0 -0
  38. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/components/auth.py +0 -0
  39. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/components/layouts.py +0 -0
  40. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/__init__.py +0 -0
  41. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/ideas.py +0 -0
  42. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/login.py +0 -0
  43. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/logout.py +0 -0
  44. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/macro.py +0 -0
  45. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/personal.py +0 -0
  46. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/sectors.py +0 -0
  47. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/pages/stocks.py +0 -0
  48. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/utils/__init__.py +0 -0
  49. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/dashboard/utils/dashboard_helper.py +0 -0
  50. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/market/__init__.py +0 -0
  51. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/market/screener.py +0 -0
  52. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/__init__.py +0 -0
  53. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/heatmap.py +0 -0
  54. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/helper.py +0 -0
  55. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/portfolio/wrappers.py +0 -0
  56. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/__init__.py +0 -0
  57. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/cli.py +0 -0
  58. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/config_helper.py +0 -0
  59. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/cq.py +0 -0
  60. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/custom_logger.py +0 -0
  61. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex/utils/mailer.py +0 -0
  62. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex.egg-info/dependency_links.txt +0 -0
  63. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex.egg-info/entry_points.txt +0 -0
  64. {folioflex-1.2.2 → folioflex-1.2.3}/folioflex.egg-info/top_level.txt +0 -0
  65. {folioflex-1.2.2 → folioflex-1.2.3}/setup.cfg +0 -0
  66. {folioflex-1.2.2 → folioflex-1.2.3}/setup.py +0 -0
  67. {folioflex-1.2.2 → folioflex-1.2.3}/tests/test_chatbot.py +0 -0
  68. {folioflex-1.2.2 → folioflex-1.2.3}/tests/test_portfolio.py +0 -0
  69. {folioflex-1.2.2 → folioflex-1.2.3}/tests/test_utils.py +0 -0
  70. {folioflex-1.2.2 → folioflex-1.2.3}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: folioflex
3
- Version: 1.2.2
3
+ Version: 1.2.3
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)
@@ -44,6 +44,7 @@ Requires-Dist: sqlalchemy>=2.0.23
44
44
  Requires-Dist: yfinance>=0.2.32
45
45
  Requires-Dist: tzlocal>=5.2
46
46
  Requires-Dist: dash>=1.0.2
47
+ Requires-Dist: dash-ag-grid>=31.2.0
47
48
  Requires-Dist: dash_bootstrap_components>=1.6.0
48
49
  Requires-Dist: dash-core-components>=1.0.0
49
50
  Requires-Dist: dash-html-components>=1.0.0
@@ -136,6 +136,12 @@ class Budget:
136
136
  """
137
137
  Modify the transactions for the budget.
138
138
 
139
+ The modifications include:
140
+ - Zero out the amount for the given columns
141
+ - Extract the text between quotes
142
+ - Preprocess the emoji in the text
143
+ - Remove the pending transactions.
144
+
139
145
  Parameters
140
146
  ----------
141
147
  tx_df : DataFrame
@@ -285,6 +291,74 @@ class Budget:
285
291
  return "Incoming Transfer"
286
292
  return "OTHER"
287
293
 
294
+ def identify_subscriptions(self, tx_df):
295
+ """
296
+ Identify possible subscriptions in the transactions.
297
+
298
+ The subscriptions are identified by the name and also the amount. If
299
+ the amount is similar for multiple transactions, and there are at least
300
+ a set amount of transcations, then it is likely a subscription.
301
+
302
+ Parameters
303
+ ----------
304
+ tx_df : DataFrame
305
+ The transactions to identify subscriptions for.
306
+
307
+ Returns
308
+ -------
309
+ subscriptions : DataFrame
310
+ The subscriptions identified in the transactions.
311
+
312
+ """
313
+ # default values
314
+ min_transactions = 3
315
+ interval_std_threshold = 5
316
+ amount_std_threshold = 0.1
317
+
318
+ # group data by name
319
+ grouped_df = tx_df.groupby("name")
320
+
321
+ # identify subscriptions
322
+ subscriptions = []
323
+ for name, group in grouped_df:
324
+ if len(group) < min_transactions:
325
+ continue
326
+
327
+ # calculating the intervals and ensure they are regular
328
+ intervals = group["date"].diff().dropna().dt.days
329
+ regular = intervals.std() <= interval_std_threshold
330
+
331
+ # calculate the amount and ensure it is consistent
332
+ amount_mean = group["amount"].mean()
333
+ if amount_mean == 0:
334
+ continue
335
+ relative_std = group["amount"].std() / amount_mean
336
+ consistent_amount = relative_std <= amount_std_threshold
337
+
338
+ # get the last date and amount
339
+ last_date = group["date"].max()
340
+ last_amount = group[group["date"] == last_date]["amount"].values[0]
341
+
342
+ if regular and consistent_amount:
343
+ subscriptions.append(
344
+ {
345
+ "Description": name,
346
+ "Occurrences": len(group),
347
+ "Mean Interval (Days)": intervals.mean(),
348
+ "Amount Mean": group["amount"].mean(),
349
+ "Amount Std Dev": group["amount"].std(),
350
+ "Last Date": last_date,
351
+ "Last Amount": last_amount,
352
+ }
353
+ )
354
+
355
+ subscriptions_df = pd.DataFrame(subscriptions)
356
+ subscriptions_df = subscriptions_df.sort_values(
357
+ by="Occurrences", ascending=False
358
+ )
359
+
360
+ return subscriptions_df
361
+
288
362
  def budget_view(self, tx_df, target_date, exclude_labels=None):
289
363
  """
290
364
  Provide a view of categories and their budget status.
@@ -308,6 +382,7 @@ class Budget:
308
382
  if exclude_labels is not None:
309
383
  tx_df = tx_df[~tx_df["label"].isin(exclude_labels)]
310
384
  budgets = {k: v for k, v in budgets.items() if k not in exclude_labels}
385
+
311
386
  # grouping transactions
312
387
  grouped_transactions = (
313
388
  tx_df[tx_df["date"].dt.to_period("M") == target_date]
@@ -316,6 +391,7 @@ class Budget:
316
391
  .reset_index()
317
392
  .sort_values("amount", ascending=True)
318
393
  )
394
+
319
395
  # adding in the budget
320
396
  unused_keys = set(budgets.keys()) - set(grouped_transactions["label"])
321
397
  unused_keys_df = pd.DataFrame({"label": list(unused_keys)})
@@ -327,6 +403,7 @@ class Budget:
327
403
  grouped_transactions["budget"] = grouped_transactions["budget"].fillna(
328
404
  float(self.config_dict["default"])
329
405
  )
406
+
330
407
  # providing a total
331
408
  totals = grouped_transactions.sum(numeric_only=True).to_frame().T
332
409
  totals["label"] = "TOTAL"
@@ -517,7 +594,78 @@ class Budget:
517
594
 
518
595
  return fig
519
596
 
520
- def category_tx_view(self, tx_df, target_date, category):
597
+ def display_category_trend(self, tx_df, category):
598
+ """
599
+ Display the category trend view as a bar chart.
600
+
601
+ Parameters
602
+ ----------
603
+ tx_df : DataFrame
604
+ The transactions to view income status for.
605
+ category : str
606
+ The category to view the trend for.
607
+
608
+ Returns
609
+ -------
610
+ fig : Figure
611
+ The category trend view as a line chart.
612
+
613
+ """
614
+ # filter data for category
615
+ category_df = tx_df[tx_df["label"] == category]
616
+ category_df = category_df.groupby(["month"])["amount"].sum().reset_index()
617
+
618
+ # budgets
619
+ category_budget = self.budgets.get(category, 0)
620
+ average_budget = category_df["amount"].mean()
621
+ twelve_month_avg = category_df["amount"].rolling(window=12).mean()
622
+
623
+ # create bar chart
624
+ fig = px.bar(
625
+ category_df,
626
+ x="month",
627
+ y="amount",
628
+ title=f"'{category}' Spending Trend",
629
+ labels={"month": "Month", "amount": "Amount"},
630
+ color_discrete_sequence=["blue"],
631
+ )
632
+
633
+ # add budget line
634
+ fig.add_scatter(
635
+ x=category_df["month"],
636
+ y=[category_budget] * len(category_df),
637
+ mode="lines",
638
+ line={"color": "red", "dash": "dash"},
639
+ name="Budget",
640
+ )
641
+
642
+ # add average line
643
+ fig.add_scatter(
644
+ x=category_df["month"],
645
+ y=[average_budget] * len(category_df),
646
+ mode="lines",
647
+ line={"color": "black", "dash": "dash"},
648
+ name="Average",
649
+ )
650
+
651
+ # add 12 month average line
652
+ fig.add_scatter(
653
+ x=category_df["month"],
654
+ y=twelve_month_avg,
655
+ mode="lines",
656
+ line={"color": "green", "dash": "dash"},
657
+ name="12-Month Average",
658
+ )
659
+
660
+ return fig
661
+
662
+ def category_tx_view(
663
+ self,
664
+ tx_df,
665
+ target_date,
666
+ category,
667
+ columns=None,
668
+ ):
521
669
  """
522
670
  Display the transactions view as a table.
523
671
 
@@ -529,6 +677,8 @@ class Budget:
529
677
  The date to view transactions for. Format: YYYY-MM.
530
678
  category : str
531
679
  The category to view transactions for.
680
+ columns : list (optional)
681
+ The columns to view in the table.
532
682
 
533
683
  Returns
534
684
  -------
@@ -536,10 +686,20 @@ class Budget:
536
686
  The category of transactions view as a table.
537
687
 
538
688
  """
689
+ if columns is None:
690
+ columns = [
691
+ "date",
692
+ "amount",
693
+ "name",
694
+ "label",
695
+ "official_name",
696
+ ]
697
+
539
698
  cat_tx_df = tx_df[
540
699
  (tx_df["date"].dt.to_period("M") == target_date)
541
700
  & (tx_df["label"].str.contains(category))
542
- ].sort_values(by="amount", ascending=False)
701
+ ].sort_values(by="date", ascending=False)
702
+ cat_tx_df = cat_tx_df[columns]
543
703
  return cat_tx_df
544
704
 
545
705
  def update_labels_db(self, tx_df, engine=None, label_column="label"):
@@ -6,6 +6,8 @@
6
6
 
7
7
  [all]
8
8
  tx_file = static.tx_file
9
+ username = static.username
10
+ password = static.password
9
11
  filter_type = static.filter_type
10
12
  filter_broker = []
11
13
  funds = static.funds
@@ -16,6 +18,8 @@ stock_splits = static.stock_splits
16
18
 
17
19
  [fidelity]
18
20
  tx_file = static.tx_file
21
+ username = static.username
22
+ password = static.password
19
23
  filter_type = static.filter_type
20
24
  filter_broker = ["fidelity"]
21
25
  funds = static.funds
@@ -26,6 +30,8 @@ stock_splits = static.stock_splits
26
30
 
27
31
  [ib]
28
32
  tx_file = static.tx_file
33
+ username = static.username
34
+ password = static.password
29
35
  filter_type = static.filter_type
30
36
  filter_broker = ["ib"]
31
37
  funds = static.funds
@@ -36,6 +42,8 @@ stock_splits = static.stock_splits
36
42
 
37
43
  [roth]
38
44
  tx_file = static.tx_file
45
+ username = static.username
46
+ password = static.password
39
47
  filter_type = static.filter_type
40
48
  filter_broker = ["roth"]
41
49
  funds = static.funds
@@ -46,6 +54,8 @@ stock_splits = static.stock_splits
46
54
 
47
55
  [ybr]
48
56
  tx_file = static.tx_file
57
+ username = static.username
58
+ password = static.password
49
59
  filter_type = static.filter_type
50
60
  filter_broker = ["ybr"]
51
61
  funds = static.funds
@@ -56,6 +66,8 @@ stock_splits = static.stock_splits
56
66
 
57
67
  [static]
58
68
  tx_file = $AWS_TX_FILE
69
+ username = ""
70
+ password = ""
59
71
  filter_type = []
60
72
  funds = ["BLKEQIX", "BLKRGIX", "BLKRVIX", "HLIEIX", "LIPIX", "TRPILCG", "TRPSV", "DODIX"]
61
73
  delisted = [
@@ -0,0 +1,105 @@
1
+ """Components - dash formats."""
2
+
3
+ from folioflex.utils import custom_logger
4
+
5
+ logger = custom_logger.setup_logging(__name__)
6
+
7
+
8
+ def get_column_defs(table):
9
+ """
10
+ Get the column definitions.
11
+
12
+ Parameters
13
+ ----------
14
+ table : pd.DataFrame
15
+ The table
16
+
17
+ Returns
18
+ -------
19
+ column_defs : list
20
+ The column definitions
21
+
22
+ """
23
+ column_defs = []
24
+ for col in table.columns:
25
+ # specific case for columns
26
+ if any(substring in col.lower() for substring in ["amount"]):
27
+ column_defs.append(
28
+ {
29
+ "field": col,
30
+ "headerName": col,
31
+ "valueFormatter": {"function": "d3.format(',.2f')(params.value)"},
32
+ }
33
+ )
34
+ # update integer columns
35
+ elif table[col].dtype.kind in "i":
36
+ if "year" in col.lower():
37
+ column_defs.append(
38
+ {
39
+ "field": col,
40
+ "headerName": col,
41
+ "valueFormatter": {"function": "d3.format('d')(params.value)"},
42
+ }
43
+ )
44
+ else:
45
+ column_defs.append(
46
+ {
47
+ "field": col,
48
+ "headerName": col,
49
+ "valueFormatter": {"function": "d3.format(',')(params.value)"},
50
+ }
51
+ )
52
+ # update float columns
53
+ elif table[col].dtype.kind in "f":
54
+ if any(substring in col.lower() for substring in ["pct"]):
55
+ column_defs.append(
56
+ {
57
+ "field": col,
58
+ "headerName": col,
59
+ "valueFormatter": {
60
+ "function": "d3.format('.2%')(params.value)"
61
+ },
62
+ }
63
+ )
64
+ else:
65
+ column_defs.append(
66
+ {
67
+ "field": col,
68
+ "headerName": col,
69
+ "valueFormatter": {
70
+ "function": "d3.format(',.1f')(params.value)"
71
+ },
72
+ }
73
+ )
74
+ # update other types
75
+ else:
76
+ column_defs.append(
77
+ {
78
+ "field": col,
79
+ "headerName": col,
80
+ }
81
+ )
82
+ return column_defs
83
+
84
+
85
+ def remove_column_defs(column_defs, col_name):
86
+ """
87
+ Remove a column from the column definitions.
88
+
89
+ Parameters
90
+ ----------
91
+ column_defs : list
92
+ The column definitions
93
+ col_name : str
94
+ The column name to remove
95
+
96
+ Returns
97
+ -------
98
+ column_defs : list
99
+ The column definitions
100
+
101
+ """
102
+ for col in column_defs:
103
+ if col["field"] == col_name:
104
+ column_defs.remove(col)
105
+ return column_defs