diff --git a/SIPPCompare.db b/SIPPCompare.db
new file mode 100644
index 0000000..0e2d14c
Binary files /dev/null and b/SIPPCompare.db differ
diff --git a/SIPPCompare.spec b/SIPPCompare.spec
index 3f784b7..4322e84 100644
--- a/SIPPCompare.spec
+++ b/SIPPCompare.spec
@@ -5,7 +5,7 @@ a = Analysis(
['src\\main.py'],
pathex=[],
binaries=[],
- datas=[('gui/*.ui', 'gui'), ('icon2.ico', '.')],
+ datas=[('gui/*.ui', 'gui'), ('gui/dialogs/*.ui', 'gui/dialogs'), ('icon2.ico', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
diff --git a/gui/dialogs/platform_rename.ui b/gui/dialogs/platform_rename.ui
new file mode 100644
index 0000000..341d53c
--- /dev/null
+++ b/gui/dialogs/platform_rename.ui
@@ -0,0 +1,99 @@
+
+
+ PlatformRename
+
+
+
+ 0
+ 0
+ 300
+ 100
+
+
+
+
+ 300
+ 100
+
+
+
+
+ 300
+ 100
+
+
+
+ Name Platform
+
+
+
+
+ 7
+ 41
+ 287
+ 22
+
+
+
+
+ 11
+
+
+
+
+
+
+ 35
+ 9
+ 241
+ 20
+
+
+
+
+ 11
+
+
+
+ Enter a new name for the platform
+
+
+
+
+
+ 220
+ 70
+ 75
+ 24
+
+
+
+
+ 10
+
+
+
+ OK
+
+
+
+
+
+
+ rename_plat_ok_but
+ clicked()
+ PlatformRename
+ accept()
+
+
+ 257
+ 71
+
+
+ 149
+ 44
+
+
+
+
+
diff --git a/gui/main_gui.ui b/gui/main_gui.ui
index 8b5f2de..f618c7f 100644
--- a/gui/main_gui.ui
+++ b/gui/main_gui.ui
@@ -220,13 +220,13 @@
File
-
+
-
+
- Edit Platforms
+ Platform List
diff --git a/gui/output_window.ui b/gui/output_window.ui
index 3ba39c9..1425a60 100644
--- a/gui/output_window.ui
+++ b/gui/output_window.ui
@@ -1,37 +1,108 @@
- ResultsWindow
-
+ OutputWindow
+
0
0
- 400
- 355
+ 1330
+ 685
- 400
- 355
+ 1330
+ 685
- 400
- 355
+ 1330
+ 685
Results
-
+
+
+
+ -10
+ -10
+ 1350
+ 650
+
+
+
+
+
+
+ 1231
+ 642
+ 91
+ 31
+
+
+
+
+ 10
+
+
+
+ Save graph
+
+
+
+
+
+ 370
+ 635
+ 581
+ 41
+
+
+
+ 1
+
+
+ 20
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSlider::TickPosition::TicksAbove
+
+
+ 1
+
+
+
+
+
+ 1134
+ 642
+ 91
+ 31
+
+
+
+
+ 10
+
+
+
+ Save CSV
+
+
+
10
- 10
- 381
- 301
+ 640
+ 341
+ 31
@@ -39,66 +110,19 @@
11
-
-
-
-
- 318
- 323
- 75
- 24
-
-
-
-
- 10
-
-
- OK
-
-
-
-
-
- 238
- 323
- 75
- 24
-
-
-
-
- 10
-
-
-
- Save
+ Fees over 1 year(s) (assuming no change in value)
-
- output
- res_save_but
- res_ok_but
-
+
+
+ MplWidget
+ QWidget
+
+ 1
+
+
-
-
- res_ok_but
- clicked(bool)
- ResultsWindow
- close()
-
-
- 357
- 281
-
-
- 199
- 149
-
-
-
-
+
diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui
index fe3a57d..f7c638f 100644
--- a/gui/platform_edit.ui
+++ b/gui/platform_edit.ui
@@ -2,6 +2,9 @@
PlatformEdit
+
+ Qt::WindowModality::ApplicationModal
+
0
@@ -10,6 +13,18 @@
567
+
+
+ 630
+ 567
+
+
+
+
+ 630
+ 567
+
+
Platform Editor
@@ -215,7 +230,7 @@
- Fund dealing fee*
+ Fund dealing fee
@@ -339,7 +354,7 @@
8
- 262
+ 540
191
21
@@ -505,6 +520,24 @@
on the value above £ there is no charge
+
+
+
+ 10
+ 284
+ 151
+ 16
+
+
+
+
+ 11
+
+
+
+ Fund platform fee*
+
+
diff --git a/gui/platform_list.ui b/gui/platform_list.ui
new file mode 100644
index 0000000..128cde3
--- /dev/null
+++ b/gui/platform_list.ui
@@ -0,0 +1,153 @@
+
+
+ PlatformList
+
+
+
+ 0
+ 0
+ 263
+ 473
+
+
+
+
+ 263
+ 473
+
+
+
+
+ 263
+ 473
+
+
+
+ Platform List
+
+
+
+
+ 10
+ 34
+ 243
+ 371
+
+
+
+
+ 11
+
+
+
+
+
+
+ 11
+ 413
+ 121
+ 24
+
+
+
+
+ 10
+
+
+
+ Add platform
+
+
+
+
+
+ 135
+ 413
+ 121
+ 24
+
+
+
+
+ 10
+
+
+
+ Remove platform
+
+
+
+
+
+ 136
+ 442
+ 121
+ 24
+
+
+
+
+ 10
+
+
+
+ Save
+
+
+
+
+
+ 11
+ 442
+ 121
+ 24
+
+
+
+
+ 10
+
+
+
+ Edit platform
+
+
+
+
+
+ 12
+ 8
+ 231
+ 20
+
+
+
+
+ 11
+
+
+
+ Platform enabled?
+
+
+
+
+
+
+ plist_save_but
+ clicked()
+ PlatformList
+ close()
+
+
+ 196
+ 453
+
+
+ 131
+ 236
+
+
+
+
+
diff --git a/src/data_struct.py b/src/data_struct.py
new file mode 100644
index 0000000..6e61055
--- /dev/null
+++ b/src/data_struct.py
@@ -0,0 +1,16 @@
+class Platform:
+ def __init__(self, plat_id, fund_plat_fee, plat_name, enabled, fund_deal_fee,
+ share_plat_fee, share_plat_max_fee, share_deal_fee,
+ share_deal_reduce_trades, share_deal_reduce_amount,
+ ):
+
+ self.plat_id = plat_id
+ self.fund_plat_fee = fund_plat_fee
+ self.plat_name = plat_name
+ self.enabled = enabled
+ self.fund_deal_fee = fund_deal_fee
+ self.share_plat_fee = share_plat_fee
+ self.share_plat_max_fee = share_plat_max_fee
+ self.share_deal_fee = share_deal_fee
+ self.share_deal_reduce_trades = share_deal_reduce_trades
+ self.share_deal_reduce_amount = share_deal_reduce_amount
diff --git a/src/db_handler.py b/src/db_handler.py
new file mode 100644
index 0000000..47c7cdf
--- /dev/null
+++ b/src/db_handler.py
@@ -0,0 +1,264 @@
+import os
+import sqlite3
+
+import resource_finder
+from data_struct import Platform
+
+
+class DBHandler:
+ def __init__(self):
+ # Function to create all necessary database tables if the DB file doesn't exist
+ def create_tables():
+ self.cur.execute("""
+ CREATE TABLE "tblPlatforms" (
+ "PlatformID" INTEGER NOT NULL UNIQUE,
+ "PlatformName" TEXT,
+ "IsEnabled" INTEGER NOT NULL,
+ PRIMARY KEY("PlatformID")
+ )
+ """)
+
+ self.cur.execute("""
+ CREATE TABLE "tblFlatPlatFees" (
+ "PlatformID" INTEGER NOT NULL UNIQUE,
+ "SharePlatFee" REAL NOT NULL,
+ "SharePlatMaxFee" REAL,
+ PRIMARY KEY("PlatformID"),
+ FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID")
+ )
+ """)
+
+ self.cur.execute("""
+ CREATE TABLE "tblFlatDealFees" (
+ "PlatformID" INTEGER NOT NULL UNIQUE,
+ "FundDealFee" REAL,
+ "ShareDealFee" REAL NOT NULL,
+ "ShareDealReduceTrades" REAL,
+ "ShareDealReduceAmount" REAL,
+ PRIMARY KEY("PlatformID"),
+ FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID")
+ )
+ """)
+
+ self.cur.execute("""
+ CREATE TABLE "tblFundPlatFee" (
+ "PlatformID" INTEGER NOT NULL,
+ "Band" REAL NOT NULL,
+ "Fee" REAL NOT NULL,
+ PRIMARY KEY("PlatformID","Band","Fee"),
+ FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID")
+ )
+ """)
+
+ self.cur.execute("""
+ CREATE TABLE "tblUserDetails" (
+ "UserID" INTEGER NOT NULL UNIQUE,
+ "PensionValue" REAL NOT NULL,
+ "SliderValue" INTEGER NOT NULL,
+ "ShareTrades" INTEGER NOT NULL,
+ "FundTrades" INTEGER NOT NULL,
+ PRIMARY KEY("UserID")
+ )
+ """)
+
+ if not os.path.exists(resource_finder.get_res_path("SIPPCompare.db")):
+ db_exists = False
+ else:
+ db_exists = True
+
+ self.conn = sqlite3.connect(resource_finder.get_res_path("SIPPCompare.db"))
+ self.cur = self.conn.cursor()
+ if not db_exists:
+ create_tables()
+
+ # Retrieve the list of platform names for list population
+ def retrieve_plat_list(self) -> list:
+ res = self.cur.execute("SELECT PlatformName FROM tblPlatforms").fetchall()
+ plat_name_list = []
+ for platform in res:
+ plat_name_list.append(platform[0])
+
+ return plat_name_list
+
+ def write_platforms(self, plat_list: list[Platform]):
+ for i in range(len(plat_list)):
+ platforms_data = [
+ plat_list[i].plat_id,
+ plat_list[i].plat_name,
+ plat_list[i].enabled
+ ]
+
+ flat_plat_fees_data = [
+ plat_list[i].plat_id,
+ plat_list[i].share_plat_fee,
+ plat_list[i].share_plat_max_fee
+ ]
+
+ flat_deal_fees_data = [
+ plat_list[i].plat_id,
+ plat_list[i].fund_deal_fee,
+ plat_list[i].share_deal_fee,
+ plat_list[i].share_deal_reduce_trades,
+ plat_list[i].share_deal_reduce_amount
+ ]
+
+ res = self.cur.execute(f"""
+ SELECT EXISTS(
+ SELECT PlatformID FROM tblPlatforms
+ WHERE PlatformID = {i}
+ )""").fetchall()
+
+ if res[0][0] == 1:
+ self.cur.execute(f"""
+ UPDATE tblPlatforms SET
+ PlatformID = ?,
+ PlatformName = ?,
+ IsEnabled = ?
+ WHERE PlatformID = {i}
+ """, platforms_data)
+
+ self.cur.execute(f"""
+ UPDATE tblFlatPlatFees SET
+ PlatformID = ?,
+ SharePlatFee = ?,
+ SharePlatMaxFee = ?
+ WHERE PlatformID = {i}
+ """, flat_plat_fees_data)
+
+ self.cur.execute(f"""
+ UPDATE tblFlatDealFees SET
+ PlatformID = ?,
+ FundDealFee = ?,
+ ShareDealFee = ?,
+ ShareDealReduceTrades = ?,
+ ShareDealReduceAmount = ?
+ WHERE PlatformID = {i}
+ """, flat_deal_fees_data)
+
+ self.cur.execute(f"DELETE FROM tblFundPlatFee WHERE PlatformID = {i}")
+ else:
+ self.cur.execute("INSERT INTO tblPlatforms VALUES (?, ?, ?)", platforms_data)
+ self.cur.execute("INSERT INTO tblFlatPlatFees VALUES (?, ?, ?)", flat_plat_fees_data)
+ self.cur.execute("INSERT INTO tblFlatDealFees VALUES (?, ?, ?, ?, ?)", flat_deal_fees_data)
+
+ exec_str = f"INSERT INTO tblFundPlatFee VALUES\n"
+ for x in range(len(plat_list[i].fund_plat_fee[0])):
+ band = plat_list[i].fund_plat_fee[0][x]
+ fee = plat_list[i].fund_plat_fee[1][x]
+ exec_str += f"({i}, {band}, {fee}),\n"
+ exec_str = exec_str[:-2]
+ self.cur.execute(exec_str)
+
+ self.conn.commit()
+
+ # Retrieve all info about all platforms in DB and initialise Platform objects
+ def retrieve_platforms(self) -> list[Platform]:
+ # Retrieve all one-to-one relations
+ platforms_res = self.cur.execute("""
+ SELECT
+ -- tblPlatforms
+ tblPlatforms.PlatformID, PlatformName, IsEnabled,
+ -- tblFlatPlatFees
+ SharePlatFee, SharePlatMaxFee,
+ -- tblFlatDealFees
+ FundDealFee,
+ ShareDealFee,
+ ShareDealReduceTrades,
+ ShareDealReduceAmount
+ FROM tblPlatforms
+ INNER JOIN tblFlatPlatFees ON
+ tblPlatforms.PlatformID = tblFlatPlatFees.PlatformID
+ INNER JOIN tblFlatDealFees ON
+ tblPlatforms.PlatformID = tblFlatDealFees.PlatformID
+ """).fetchall()
+
+ platforms = []
+ for platform in platforms_res:
+ # plat_id, plat_name, enabled, share_plat_fee, share_plat_max_fee, fund_deal_fee,
+ # share_deal_fee, share_deal_reduce_trades, share_deal_reduce_amount
+ this_platform = [platform[0], platform[1], platform[2], platform[3], platform[4],
+ platform[5], platform[6], platform[7], platform[8]]
+ platforms.append(this_platform)
+
+ # Insert 2D array into each platform data in preparation for fund_plat_fee retrival
+ for platform in platforms:
+ platform.insert(1, [[], []])
+
+ # Get all records from tblFundPlatFee, add them to the platforms list based on ID
+ # WARNING: This code is dependent on PlatformID being sequential from 0 in DB records
+ fund_plat_fee_res = self.cur.execute("SELECT * FROM tblFundPlatFee ORDER BY PlatformID ASC").fetchall()
+ for i in range(len(fund_plat_fee_res)):
+ plat_id = fund_plat_fee_res[i][0]
+ platforms[plat_id][1][0].append(fund_plat_fee_res[i][1])
+ platforms[plat_id][1][1].append(fund_plat_fee_res[i][2])
+
+ platform_obj_list: list[Platform] = []
+ for platform in platforms:
+ platform_obj_list.append(Platform(
+ platform[0], platform[1], platform[2], platform[3],
+ platform[6], platform[4], platform[5], platform[7],
+ platform[8], platform[9]
+ ))
+
+ return platform_obj_list
+
+ # This function writes the details the user entered this session to the DB
+ def write_user_details(self, pension_val: float, slider_val: int, share_trades: int, fund_trades: int):
+ # Hardcode UserID as 0
+ user_details_data = (0, pension_val, slider_val, share_trades, fund_trades)
+
+ # Check if there is already a record in tblUserDetails
+ res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone()
+ if res[0] == 0:
+ # If there isn't then insert a new record
+ self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?, ?)", user_details_data)
+ else:
+ # If there is then update the existing record (only ever one record as of now)
+ self.cur.execute("""
+ UPDATE tblUserDetails SET
+ UserID = ?,
+ PensionValue = ?,
+ SliderValue = ?,
+ ShareTrades = ?,
+ FundTrades = ?
+ """, user_details_data)
+ self.conn.commit()
+
+ # Function to retrieve details entered by the user in prev session from DB
+ def retrieve_user_details(self) -> dict:
+ res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone()
+ if res[0] == 0:
+ return {"NO_RECORD": None}
+
+ res = self.cur.execute("SELECT * FROM tblUserDetails")
+ res_tuple: tuple = res.fetchone()
+ user_details_dict: dict[str, float | int] = {
+ "user_id": res_tuple[0],
+ "pension_val": res_tuple[1],
+ "slider_val": res_tuple[2],
+ "share_trades": res_tuple[3],
+ "fund_trades": res_tuple[4]
+ }
+
+ return user_details_dict
+
+ def toggle_platform_state(self, index: int, state: bool):
+ self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", [state, index])
+ self.conn.commit()
+
+ def remove_platform(self, index: int):
+ tbl_list = ["tblPlatforms", "tblFlatPlatFees", "tblFlatDealFees", "tblFundPlatFee"]
+ for tbl in tbl_list:
+ self.cur.execute(f"DELETE FROM {tbl} WHERE PlatformID = {index}")
+
+ res = self.cur.execute("SELECT PlatformID from tblPlatforms").fetchall()
+ n = len(res)
+ for i in range(n):
+ for tbl in tbl_list:
+ self.cur.execute(f"""
+ UPDATE {tbl}
+ SET PlatformID = {index + i}
+ WHERE PlatformID = {index + 1 + i}
+ """)
+
+ self.conn.commit()
diff --git a/src/main.py b/src/main.py
index 14c9b4d..2517bfe 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,20 +1,11 @@
-from PyQt6.QtWidgets import QApplication
-
import sys
-import platform_edit
+from PyQt6.QtWidgets import QApplication
+
+from main_window import SIPPCompare
app = QApplication(sys.argv)
-# Show platform edit window first, before main win
-# When debugging, can be useful to autofill values to save time
-if len(sys.argv) > 1:
- if sys.argv[1] == "--DEBUG_AUTOFILL":
- window = platform_edit.PlatformEdit(True)
- else:
- window = platform_edit.PlatformEdit(False)
-else:
- window = platform_edit.PlatformEdit(False)
-
+window = SIPPCompare()
window.show()
app.exec()
diff --git a/src/main_window.py b/src/main_window.py
index d56484a..d35fc3e 100644
--- a/src/main_window.py
+++ b/src/main_window.py
@@ -1,48 +1,41 @@
-from PyQt6.QtGui import QIntValidator, QIcon
-from PyQt6.QtWidgets import QMainWindow, QWidget
from PyQt6 import uic
+from PyQt6.QtGui import QIntValidator, QIcon
+from PyQt6.QtWidgets import QMainWindow, QApplication
-import output_window
import resource_finder
+from db_handler import DBHandler
+from platform_list import PlatformList
+from output_window import OutputWindow
class SIPPCompare(QMainWindow):
- # Receive instance of PlatformEdit() as parameter
- def __init__(self, plat_edit_win: QWidget):
+ def __init__(self):
super().__init__()
# Import Qt Designer UI XML file
uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables
- # Inputs
- self.optional_boxes = []
- self.fund_plat_fee = 0.0
- self.plat_name = ""
- self.fund_deal_fee = 0.0
- self.share_plat_fee = 0.0
- self.share_plat_max_fee = 0.0
- self.share_deal_fee = 0.0
- self.share_deal_reduce_trades = 0.0
- self.share_deal_reduce_amount = 0.0
-
# Results
self.fund_plat_fees = 0.0
self.fund_deal_fees = 0.0
self.share_plat_fees = 0.0
self.share_deal_fees = 0.0
+ self.results = []
# Create window objects
- self.platform_win = plat_edit_win
- self.output_win = output_window.OutputWindow()
+ self.db = DBHandler()
+ self.platform_list_win = PlatformList(self.db)
+ self.output_win = OutputWindow()
# Handle events
self.calc_but.clicked.connect(self.calculate_fees)
- # Menu bar entry (File -> Edit Platforms)
- self.actionEdit_Platforms.triggered.connect(self.show_platform_edit)
+ # Menu bar entry (File -> Platform List)
+ self.actionList_Platforms.triggered.connect(self.show_platform_list)
# Update percentage mix label when slider moved
self.mix_slider.valueChanged.connect(self.update_slider_lab)
self.value_input.valueChanged.connect(self.check_valid)
+ # Validate input
self.share_trades_combo.currentTextChanged.connect(self.check_valid)
self.fund_trades_combo.currentTextChanged.connect(self.check_valid)
@@ -50,12 +43,22 @@ class SIPPCompare(QMainWindow):
self.share_trades_combo.setValidator(QIntValidator(0, 999))
self.fund_trades_combo.setValidator(QIntValidator(0, 99))
+ # Restore last session
+ prev_session_data = self.db.retrieve_user_details()
+ if "NO_RECORD" not in prev_session_data:
+ self.value_input.setValue(prev_session_data["pension_val"])
+ self.mix_slider.setValue(prev_session_data["slider_val"])
+ self.share_trades_combo.setCurrentText(str(prev_session_data["share_trades"]))
+ self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"]))
+ self.calc_but.setFocus()
+
# Display slider position as mix between two nums (funds/shares)
def update_slider_lab(self):
slider_val = self.mix_slider.value()
mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)"
self.mix_lab.setText(mix_lab_str)
+ # Ensure that trade fields aren't blank and pension value > 0
def check_valid(self):
if self.share_trades_combo.currentText() != "" \
and self.fund_trades_combo.currentText() != "" \
@@ -64,90 +67,71 @@ class SIPPCompare(QMainWindow):
else:
self.calc_but.setEnabled(False)
- # Get variables from platform editor input fields
- def init_variables(self):
- self.optional_boxes = self.platform_win.get_optional_boxes()
- self.fund_plat_fee = self.platform_win.get_fund_plat_fee()
- self.share_plat_fee = self.platform_win.get_share_plat_fee()
- self.share_deal_fee = self.platform_win.get_share_deal_fee()
-
- # TODO: This is HORRIBLE - find better way of doing it! (maybe enums?)
- if self.optional_boxes[0]:
- self.plat_name = self.platform_win.get_plat_name()
- else:
- self.plat_name = None
-
- if self.optional_boxes[1]:
- self.fund_deal_fee = self.platform_win.get_fund_deal_fee()
- else:
- self.fund_deal_fee = None
-
- if self.optional_boxes[2]:
- self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee()
- else:
- self.share_plat_max_fee = None
-
- if self.optional_boxes[3]:
- self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades()
- else:
- self.share_deal_reduce_trades = None
-
- if self.optional_boxes[4]:
- self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount()
- else:
- self.share_deal_reduce_amount = None
-
- # Calculate fees
+ # Calculate fees for all active platforms
def calculate_fees(self):
- self.init_variables()
- # Set to zero each time to avoid persistence
- self.fund_plat_fees = 0
+ # Set to empty list each time to avoid persistence
+ self.results = []
+
+ # Get user input
value_num = float(self.value_input.value())
- # Funds/shares mix
slider_val: int = self.mix_slider.value()
- funds_value = (slider_val / 100) * value_num
fund_trades_num = int(self.fund_trades_combo.currentText())
- if self.fund_deal_fee is not None:
- self.fund_deal_fees = fund_trades_num * self.fund_deal_fee
-
- for i in range(1, len(self.fund_plat_fee[0])):
- band = self.fund_plat_fee[0][i]
- prev_band = self.fund_plat_fee[0][i - 1]
- fee = self.fund_plat_fee[1][i]
- gap = (band - prev_band)
-
- if funds_value > gap:
- self.fund_plat_fees += gap * (fee / 100)
- funds_value -= gap
- else:
- self.fund_plat_fees += funds_value * (fee / 100)
- break
-
- shares_value = (1 - (slider_val / 100)) * value_num
- if self.share_plat_max_fee is not None:
- if (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee:
- self.share_plat_fees = self.share_plat_max_fee * 12
- else:
- self.share_plat_fees = self.share_plat_fee * shares_value
-
share_trades_num = int(self.share_trades_combo.currentText())
- if self.share_deal_reduce_trades is not None:
- if (share_trades_num / 12) >= self.share_deal_reduce_trades:
- self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num
- else:
- self.share_deal_fees = self.share_deal_fee * share_trades_num
+ shares_value = (1 - (slider_val / 100)) * value_num
+ for platform in self.platform_list_win.plat_list:
+ if not platform.enabled:
+ continue
+
+ fund_plat_fees = 0.0
+ fund_deal_fees = 0.0
+ share_plat_fees = 0.0
+ share_deal_fees = 0.0
+ plat_name = platform.plat_name
+
+ if platform.fund_deal_fee is not None:
+ fund_deal_fees = fund_trades_num * platform.fund_deal_fee
+
+ funds_value = (slider_val / 100) * value_num
+ for i in range(1, len(platform.fund_plat_fee[0])):
+ band = platform.fund_plat_fee[0][i]
+ prev_band = platform.fund_plat_fee[0][i - 1]
+ fee = platform.fund_plat_fee[1][i]
+ gap = (band - prev_band)
+
+ if funds_value > gap:
+ fund_plat_fees += gap * (fee / 100)
+ funds_value -= gap
+ else:
+ fund_plat_fees += funds_value * (fee / 100)
+ break
+
+ if platform.share_plat_max_fee is not None:
+ if (platform.share_plat_fee * shares_value / 12) > platform.share_plat_max_fee:
+ share_plat_fees = platform.share_plat_max_fee * 12
+ else:
+ share_plat_fees = platform.share_plat_fee * shares_value
+
+ if platform.share_deal_reduce_trades is not None:
+ if (share_trades_num / 12) >= platform.share_deal_reduce_trades:
+ share_deal_fees = platform.share_deal_reduce_amount * share_trades_num
+ else:
+ share_deal_fees = platform.share_deal_fee * share_trades_num
+
+ self.results.append([fund_plat_fees, fund_deal_fees, share_plat_fees, share_deal_fees, plat_name])
+
+ # Save details entered by user for next session
+ self.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num)
self.show_output_win()
# Show the output window - this func is called from calculate_fee()
def show_output_win(self):
# Refresh the results when new fees are calculated
- self.output_win.display_output(self.fund_plat_fees, self.fund_deal_fees,
- self.share_plat_fees, self.share_deal_fees,
- self.plat_name
- )
+ self.output_win.display_output(self.results, 1)
+ self.output_win.activateWindow()
+ self.output_win.raise_()
self.output_win.show()
+ QApplication.alert(self.output_win)
- # Show the platform editor window (currently run-time only)
- def show_platform_edit(self):
- self.platform_win.show()
+ def show_platform_list(self):
+ self.platform_list_win.show()
diff --git a/src/output_window.py b/src/output_window.py
index 06933da..aad49d1 100644
--- a/src/output_window.py
+++ b/src/output_window.py
@@ -1,11 +1,32 @@
-from PyQt6.QtGui import QIcon
-from PyQt6.QtWidgets import QWidget
-from PyQt6 import uic
-
-import datetime
import os
+from datetime import datetime
+
+from PyQt6 import uic
+from PyQt6.QtGui import QIcon, QFont
+from PyQt6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialogButtonBox
import resource_finder
+from widgets.mpl_widget import MplWidget
+
+
+class SaveFailure(QMessageBox):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
+ self.setWindowTitle("Save failure")
+
+ self.setIcon(QMessageBox.Icon.Critical)
+ font = QFont()
+ font.setPointSize(11)
+ self.setFont(font)
+ self.setText("Failed to save file")
+ self.setDetailedText(
+ "This could be due to a permissions issue, or the file being in use by another process"
+ )
+
+ self.setStandardButtons(QMessageBox.StandardButton.Ok)
+ font.setPointSize(10)
+ self.findChild(QDialogButtonBox).setFont(font)
class OutputWindow(QWidget):
@@ -15,50 +36,103 @@ class OutputWindow(QWidget):
uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
- # Initialise class variables
- self.results_str = ""
- self.platform_name = ""
+ # Define class variables
+ self.save_err_dialog = SaveFailure()
+ self.canvas = self.graphWidget.canvas
+ self.ax = self.canvas.axes
+ self.fig = self.canvas.figure
+ self.results = []
# Handle events
- self.res_save_but.clicked.connect(self.save_results)
+ self.save_graph_but.clicked.connect(self.save_graph)
+ self.save_csv_but.clicked.connect(self.save_csv)
+ self.time_slider.valueChanged.connect(self.change_time)
- def save_results(self):
- # Use datatime for txt filename
- cur_time = datetime.datetime.now()
- if not os.path.exists("output"):
- os.makedirs("output")
- filename_str = f"output/"
- if self.platform_name is not None:
- filename_str += f"{self.platform_name}"
- else:
- filename_str += "Unnamed"
- filename_str += f"-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt"
- output_file = open(filename_str, "wt", encoding = "utf-8")
- output_file.write(self.results_str)
+ def display_output(self, results: list, years: int):
+ self.results = results
+ self.ax.clear()
+ self.ax.cla()
+ self.canvas.draw_idle()
- # Display fees in output window as plaintext readout
- def display_output(self, fund_plat_fees: float, fund_deal_fees: float,
- share_plat_fees: float, share_deal_fees: float, plat_name: str):
- self.platform_name = plat_name
- if self.platform_name is not None:
- self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):"
- else:
- self.results_str = f"Fees breakdown:"
+ names = []
+ values = []
+ for result in results:
+ names.append(result[4])
+ values.append(sum(result[:4]) * years)
- self.results_str += "\n\nPlatform fees:"
- # :.2f is used in order to display 2 decimal places (currency form)
- self.results_str += f"\n\tFund platform fees: £{round(fund_plat_fees, 2):.2f}"
- self.results_str += f"\n\tShare platform fees: £{round(share_plat_fees, 2):.2f}"
- total_plat_fees = fund_plat_fees + share_plat_fees
- self.results_str += f"\n\tTotal platform fees: £{round(total_plat_fees, 2):.2f}"
+ names = sorted(names, key=lambda x: values[names.index(x)], reverse=True)
+ values = sorted(values, reverse=True)
- self.results_str += "\n\nDealing fees:"
- self.results_str += f"\n\tFund dealing fees: £{round(fund_deal_fees, 2):.2f}"
- self.results_str += f"\n\tShare dealing fees: £{round(share_deal_fees, 2):.2f}"
- total_deal_fees = fund_deal_fees + share_deal_fees
- self.results_str += f"\n\tTotal dealing fees: £{round(total_deal_fees, 2):.2f}"
+ h_bars = self.ax.barh(names, values)
+ self.ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues])
- total_fees = total_plat_fees + total_deal_fees
- self.results_str += f"\n\nTotal fees: £{round(total_fees, 2):.2f}"
+ def save_graph(self):
+ file_picker = QFileDialog(self)
+ file_picker.setFileMode(QFileDialog.FileMode.AnyFile)
+ file_picker.setDefaultSuffix("png")
+ file_picker.setWindowTitle("Save results as PNG")
+ file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
+ file_picker.setNameFilter("*.png")
+ file_path = ""
+ cur_time = datetime.now()
+ filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.png"
+ file_picker.selectFile(filename_str)
+ if file_picker.exec():
+ file_path = file_picker.selectedFiles()[0]
- self.output.setText(self.results_str)
+ try:
+ self.fig.savefig(file_path, dpi=150)
+ except OSError:
+ self.save_err_dialog.exec()
+
+ def save_csv(self):
+ # TODO: Sort CSV output, either alphabetically or by total fees
+ file_picker = QFileDialog(self)
+ file_picker.setFileMode(QFileDialog.FileMode.AnyFile)
+ file_picker.setDefaultSuffix("csv")
+ file_picker.setWindowTitle("Save results as CSV")
+ file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
+ file_picker.setNameFilter("*.csv")
+ file_path = ""
+ cur_time = datetime.now()
+ filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.csv"
+ file_picker.selectFile(filename_str)
+ if file_picker.exec():
+ file_path = file_picker.selectedFiles()[0]
+
+ try:
+ csvfile = open(file_path, "wt")
+ csv_string = (
+ "Platform Name,Fund Platform Fee,Share Platform Fee,Fund Dealing Fee,"
+ "Share Dealing Fee,Total Platform Fees,Total Dealing Fees,Total Fund Fees,"
+ "Total Share Fees,Total Fees"
+ )
+
+ for result in self.results:
+ csv_string += '\n'
+ pn = result[4]
+ fpf = result[0]
+ spf = result[2]
+ fdf = result[1]
+ sdf = result[3]
+
+ tpf = fpf + spf
+ tdf = sdf + fdf
+
+ tff = fpf + fdf
+ tsf = spf + sdf
+ tf = tff + tsf
+ csv_string += (
+ f"{pn},\"£{fpf:,.2f}\",\"£{spf:,.2f}\",\"£{fdf:,.2f}\",\"£{sdf:,.2f}\","
+ f"\"£{tpf:,.2f}\",\"£{tdf:,.2f}\",\"£{tff:,.2f}\",\"£{tsf:,.2f}\",\"£{tf:,.2f}\""
+ )
+
+ csvfile.write(csv_string)
+ csvfile.close()
+ except OSError:
+ self.save_err_dialog.exec()
+
+ def change_time(self):
+ years: int = self.time_slider.value()
+ self.time_lab.setText(f"Fees over {years} year(s) (assuming no change in value)")
+ self.display_output(self.results, years)
diff --git a/src/platform_edit.py b/src/platform_edit.py
index 663d8f8..a3d14a6 100644
--- a/src/platform_edit.py
+++ b/src/platform_edit.py
@@ -1,39 +1,29 @@
-from PyQt6.QtCore import QRegularExpression
+from PyQt6 import uic
+from PyQt6.QtCore import QRegularExpression, QRect
from PyQt6.QtGui import QRegularExpressionValidator, QFont, QIcon
from PyQt6.QtWidgets import QWidget, QLabel
-from PyQt6 import uic
-from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
-import main_window
import resource_finder
+from db_handler import DBHandler
+from data_struct import Platform
+from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
class PlatformEdit(QWidget):
- def __init__(self, autofill: bool):
+ def __init__(self, plat: Platform):
super().__init__()
# Import Qt Designer UI XML file
uic.loadUi(resource_finder.get_res_path("gui/platform_edit.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables
- # Create main window object, passing this instance as param
- self.main_win = main_window.SIPPCompare(self)
-
- self.fund_plat_fee = []
- self.plat_name = ""
- self.fund_deal_fee = 0.0
- self.share_plat_fee = 0.0
- self.share_plat_max_fee = 0.0
- self.share_deal_fee = 0.0
- self.share_deal_reduce_trades = 0.0
- self.share_deal_reduce_amount = 0.0
+ self.plat = plat
+ self.fund_plat_fee = self.plat.fund_plat_fee
self.widgets_list_list = []
-
- self.fund_fee_rows = 1
- # Debugging feature: set with "--DEBUG_AUTOFILL" cmd argument
- self.autofill = autofill
- if autofill:
- self.save_but.setEnabled(True)
+ if len(self.plat.fund_plat_fee[0]) > 1:
+ self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - 1
+ else:
+ self.fund_fee_rows = 1
self.required_fields = [
self.share_plat_fee_box,
@@ -64,6 +54,57 @@ class PlatformEdit(QWidget):
False
]
+ # Set optional checkboxes based on DB storage
+ if self.plat.plat_name is None:
+ self.check_boxes_ticked[0] = False
+ self.plat_name_check.setChecked(False)
+ else:
+ self.check_boxes_ticked[0] = True
+ self.plat_name_check.setChecked(True)
+ self.plat_name_box.setText(self.plat.plat_name)
+
+ if self.plat.fund_deal_fee is None:
+ self.check_boxes_ticked[1] = False
+ self.fund_deal_fee_check.setChecked(False)
+ else:
+ self.check_boxes_ticked[1] = True
+ self.fund_deal_fee_check.setChecked(True)
+ self.fund_deal_fee_box.setValue(self.plat.fund_deal_fee)
+
+ self.share_plat_fee_box.setValue(self.plat.share_plat_fee * 100)
+
+ if self.plat.share_plat_max_fee is None:
+ self.check_boxes_ticked[2] = False
+ self.share_plat_max_fee_check.setChecked(False)
+ else:
+ self.check_boxes_ticked[2] = True
+ self.share_plat_max_fee_check.setChecked(True)
+ self.share_plat_max_fee_box.setValue(self.plat.share_plat_max_fee)
+
+ self.share_deal_fee_box.setValue(self.plat.share_deal_fee)
+
+ if self.plat.share_deal_reduce_trades is None:
+ self.check_boxes_ticked[3] = False
+ self.share_deal_reduce_trades_check.setChecked(False)
+ else:
+ self.check_boxes_ticked[3] = True
+ self.share_deal_reduce_trades_check.setChecked(True)
+ self.share_deal_reduce_trades_box.setValue(int(self.plat.share_deal_reduce_trades))
+
+ if self.plat.share_deal_reduce_trades is None:
+ self.check_boxes_ticked[4] = False
+ self.share_deal_reduce_amount_check.setChecked(False)
+ else:
+ self.check_boxes_ticked[4] = True
+ self.share_deal_reduce_amount_check.setChecked(True)
+ self.share_deal_reduce_amount_box.setValue(self.plat.share_deal_reduce_amount)
+
+ # Populate fund platform fee rows from DB
+ if len(self.plat.fund_plat_fee[0]) > 1:
+ self.first_tier_box.setValue(self.plat.fund_plat_fee[0][1])
+ self.first_tier_fee_box.setValue(self.plat.fund_plat_fee[1][1])
+ self.add_row(loading=True)
+
# Handle events
for field in self.required_fields:
field.valueChanged.connect(self.check_valid)
@@ -93,7 +134,7 @@ class PlatformEdit(QWidget):
QRegularExpressionValidator(QRegularExpression("\\w*"))
)
- def create_plat_fee_struct(self):
+ def create_plat_fee_struct(self) -> list:
plat_fee_struct = [[0], [0]]
plat_fee_struct[0].append(self.first_tier_box.value())
plat_fee_struct[1].append(self.first_tier_fee_box.value())
@@ -108,32 +149,34 @@ class PlatformEdit(QWidget):
# Get fee structure variables from user input when "Save" clicked
def init_variables(self):
- # If debugging, save time by hardcoding
- if self.autofill:
- self.plat_name = "AJBell"
- self.fund_plat_fee = [
- [0, 250000, 1000000, 2000000],
- [0, 0.25, 0.1, 0.05]
- ]
- self.fund_deal_fee = 1.50
- self.share_plat_fee = 0.0025
- self.share_plat_max_fee = 3.50
- self.share_deal_fee = 5.00
- self.share_deal_reduce_trades = 10
- self.share_deal_reduce_amount = 3.50
- self.check_boxes_ticked = [True, True, True, True, True]
- else:
- self.plat_name = self.plat_name_box.text()
- self.fund_plat_fee = self.create_plat_fee_struct()
- self.fund_deal_fee = float(self.fund_deal_fee_box.value())
- self.share_plat_fee = float(self.share_plat_fee_box.value()) / 100
- self.share_plat_max_fee = float(self.share_plat_max_fee_box.value())
- self.share_deal_fee = float(self.share_deal_fee_box.value())
- self.share_deal_reduce_trades = float(self.share_deal_reduce_trades_box.value())
- self.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value())
+ self.plat.fund_plat_fee = self.create_plat_fee_struct()
+ self.plat.share_plat_fee = float(self.share_plat_fee_box.value()) / 100
+ self.plat.share_deal_fee = float(self.share_deal_fee_box.value())
- # Once user input is received show main window
- self.main_win.show()
+ if self.check_boxes_ticked[0]:
+ self.plat.plat_name = self.plat_name_box.text()
+ else:
+ self.plat.plat_name = None
+
+ if self.check_boxes_ticked[1]:
+ self.plat.fund_deal_fee = float(self.fund_deal_fee_box.value())
+ else:
+ self.plat.fund_deal_fee = None
+
+ if self.check_boxes_ticked[2]:
+ self.plat.share_plat_max_fee = float(self.share_plat_max_fee_box.value())
+ else:
+ self.plat.share_plat_max_fee = None
+
+ if self.check_boxes_ticked[3]:
+ self.plat.share_deal_reduce_trades = int(self.share_deal_reduce_trades_box.value())
+ else:
+ self.plat.share_deal_reduce_trades = None
+
+ if self.check_boxes_ticked[4]:
+ self.plat.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value())
+ else:
+ self.plat.share_deal_reduce_amount = None
# This method does multiple things in order to validate the user's inputs:
# 1) Check all required fields have a non-zero value
@@ -215,50 +258,67 @@ class PlatformEdit(QWidget):
max_band = self.first_tier_box.value()
self.val_above_lab.setText(f"on the value above £{int(max_band)} there is no charge")
- def add_row(self):
- widgets = []
- font = QFont()
- font.setPointSize(11)
+ def add_row(self, loading: bool = False):
+ if loading:
+ rows_needed = self.fund_fee_rows - 1
+ else:
+ rows_needed = 1
- widgets.append(QLabel(self.gridLayoutWidget_2))
- widgets[0].setFont(font)
+ for x in range(rows_needed):
+ widgets = []
+ font = QFont()
+ font.setPointSize(11)
- widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
- widgets[1].setPrefix("£")
- widgets[1].setMaximum(9999999)
- widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons)
- widgets[1].setFont(font)
- widgets[1].valueChanged.connect(self.check_valid)
- widgets[1].valueChanged.connect(self.update_tier_labels)
+ widgets.append(QLabel(self.gridLayoutWidget_2))
+ widgets[0].setFont(font)
- widgets.append(QLabel(self.gridLayoutWidget_2))
- widgets[2].setText(f"the fee is")
- widgets[2].setFont(font)
+ widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
+ widgets[1].setPrefix("£")
+ widgets[1].setMaximum(9999999)
+ widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons)
+ widgets[1].setFont(font)
+ if loading:
+ widgets[1].setValue(self.plat.fund_plat_fee[0][x+2])
+ widgets[1].valueChanged.connect(self.check_valid)
+ widgets[1].valueChanged.connect(self.update_tier_labels)
- widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
- widgets[3].setSuffix("%")
- widgets[3].setMaximum(100)
- widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons)
- widgets[3].setFont(font)
- widgets[3].valueChanged.connect(self.check_valid)
+ widgets.append(QLabel(self.gridLayoutWidget_2))
+ widgets[2].setText(f"the fee is")
+ widgets[2].setFont(font)
- # TODO: why 28.5?
- self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0)))
- for i in range(len(widgets)):
- self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1)
+ widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
+ widgets[3].setSuffix("%")
+ widgets[3].setMaximum(100)
+ widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons)
+ widgets[3].setFont(font)
+ if loading:
+ widgets[3].setValue(self.plat.fund_plat_fee[1][x+2])
+ widgets[3].valueChanged.connect(self.check_valid)
- self.fund_fee_rows += 1
+ if loading:
+ grid_height = int(round(28.5 * self.fund_fee_rows))
+ else:
+ grid_height = int(round(28.5 * (self.fund_fee_rows + 1)))
+ self.gridLayoutWidget_2.setGeometry(QRect(11, 309, 611, grid_height))
+ for i in range(len(widgets)):
+ if loading:
+ self.gridLayout_2.addWidget(widgets[i], x + 1, i, 1, 1)
+ else:
+ self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1)
- self.widgets_list_list.append(widgets)
- cur_label_idx = self.gridLayout_2.indexOf(widgets[0])
- cur_box_idx = self.gridLayout_2.indexOf(widgets[1])
- cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2]
- cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2]
+ if not loading:
+ self.fund_fee_rows += 1
- prev_box_row = cur_box_pos[0] - 1
- prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget()
- cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget()
- cur_label_item.setText(f"between £{int(prev_box_item.value())} and")
+ self.widgets_list_list.append(widgets)
+ cur_label_idx = self.gridLayout_2.indexOf(widgets[0])
+ cur_box_idx = self.gridLayout_2.indexOf(widgets[1])
+ cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2]
+ cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2]
+
+ prev_box_row = cur_box_pos[0] - 1
+ prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget()
+ cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget()
+ cur_label_item.setText(f"between £{int(prev_box_item.value())} and")
if self.fund_fee_rows > 1:
self.del_row_but.setEnabled(True)
@@ -267,8 +327,9 @@ class PlatformEdit(QWidget):
self.add_row_but.setEnabled(False)
self.check_valid()
+ self.update_tier_labels()
- # TODO: Tab order
+ # TODO: Tab/focus order
def remove_row(self):
for widget in self.widgets_list_list[self.fund_fee_rows - 2]:
@@ -286,31 +347,3 @@ class PlatformEdit(QWidget):
self.check_valid()
self.update_tier_labels()
-
- # Getter functions (is this necessary? maybe directly reading class vars would be best...)
- def get_optional_boxes(self):
- return self.check_boxes_ticked
-
- def get_plat_name(self):
- return self.plat_name
-
- def get_fund_plat_fee(self):
- return self.fund_plat_fee
-
- def get_fund_deal_fee(self):
- return self.fund_deal_fee
-
- def get_share_plat_fee(self):
- return self.share_plat_fee
-
- def get_share_plat_max_fee(self):
- return self.share_plat_max_fee
-
- def get_share_deal_fee(self):
- return self.share_deal_fee
-
- def get_share_deal_reduce_trades(self):
- return self.share_deal_reduce_trades
-
- def get_share_deal_reduce_amount(self):
- return self.share_deal_reduce_amount
diff --git a/src/platform_list.py b/src/platform_list.py
new file mode 100644
index 0000000..77574e1
--- /dev/null
+++ b/src/platform_list.py
@@ -0,0 +1,148 @@
+from PyQt6 import uic
+from PyQt6.QtCore import QRegularExpression
+from PyQt6.QtGui import QIcon, QRegularExpressionValidator, QFont
+from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog, QDialogButtonBox, QMessageBox
+
+import resource_finder
+from db_handler import DBHandler
+from data_struct import Platform
+from platform_edit import PlatformEdit
+
+
+class PlatformRename(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ # Import Qt Designer UI XML file
+ uic.loadUi(resource_finder.get_res_path("gui/dialogs/platform_rename.ui"), self)
+ self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
+
+ self.rename_plat_box.setFocus()
+ self.new_name = ""
+
+ # Set validators
+ # Regex accepts any characters that match [a-Z], [0-9] or _
+ self.rename_plat_box.setValidator(
+ QRegularExpressionValidator(QRegularExpression("\\w*"))
+ )
+
+ self.rename_plat_ok_but.clicked.connect(self.store_new_name)
+
+ def store_new_name(self):
+ self.new_name = self.rename_plat_box.text()
+
+ def closeEvent(self, event):
+ event.ignore()
+ self.reject()
+
+
+class RemoveConfirm(QMessageBox):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
+ self.setWindowTitle("Remove platform?")
+ self.setIcon(QMessageBox.Icon.Warning)
+ font = QFont()
+ font.setPointSize(11)
+ self.setFont(font)
+ self.setText("Are you sure you want to remove this platform?")
+ self.setInformativeText("This action is immediate and permanent")
+
+ self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
+ self.setDefaultButton(QMessageBox.StandardButton.Cancel)
+ font.setPointSize(10)
+ self.findChild(QDialogButtonBox).setFont(font)
+
+
+class PlatformList(QWidget):
+ def __init__(self, db: DBHandler):
+ super().__init__()
+ # Import Qt Designer UI XML file
+ uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self)
+ self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
+
+ # Initialise class variables
+ self.db = db
+ self.plat_edit_win = None
+ self.plat_list_dialog = PlatformRename()
+ self.del_plat_dialog = RemoveConfirm()
+ self.plat_list = []
+ self.plat_name_list = []
+ self.new_plat_name = ""
+ self.update_plat_list()
+ self.db_indices = [x for x in range(len(self.plat_name_list))]
+
+ # Handle events
+ self.add_plat_but.clicked.connect(self.add_platform)
+ self.del_plat_but.clicked.connect(self.remove_platform)
+ self.edit_plat_but.clicked.connect(self.edit_platform)
+ self.plist_save_but.clicked.connect(self.save_platforms)
+ self.plat_enabled_check.checkStateChanged.connect(self.toggle_platform_state)
+ self.platListWidget.currentRowChanged.connect(self.get_enabled_state)
+
+ def update_plat_list(self):
+ self.plat_name_list = self.db.retrieve_plat_list()
+ self.plat_list = self.db.retrieve_platforms()
+ self.platListWidget.clear()
+
+ for i in range(len(self.plat_name_list)):
+ plat_name = self.plat_name_list[i]
+ item = QListWidgetItem()
+ if plat_name is not None:
+ item.setText(plat_name)
+ else:
+ item.setText(f"Unnamed [ID: {i}]")
+
+ self.platListWidget.addItem(item)
+
+ def add_platform(self):
+ name_dialog_res = self.plat_list_dialog.exec()
+ if name_dialog_res == QDialog.DialogCode.Accepted:
+ name = self.plat_list_dialog.new_name
+ index = self.platListWidget.count()
+ if name != "":
+ self.platListWidget.addItem(name)
+ name_param = name
+ else:
+ self.platListWidget.addItem(f"Unnamed [ID: {index}]")
+ name_param = None
+
+ self.plat_list.append(Platform(
+ index, [[0], [0]], name_param, True, 0, 0, None, 0, None, None)
+ )
+ self.plat_edit_win = PlatformEdit(self.plat_list[index])
+ self.plat_edit_win.show()
+
+ def get_enabled_state(self):
+ index = self.platListWidget.currentRow()
+ is_enabled = self.plat_list[index].enabled
+ if is_enabled:
+ self.plat_enabled_check.setChecked(True)
+ else:
+ self.plat_enabled_check.setChecked(False)
+
+ if index in self.db_indices:
+ self.del_plat_but.setEnabled(True)
+ else:
+ self.del_plat_but.setEnabled(False)
+
+ def edit_platform(self):
+ index = self.platListWidget.currentRow()
+ self.plat_edit_win = PlatformEdit(self.plat_list[index])
+ self.plat_edit_win.show()
+
+ def save_platforms(self):
+ self.db.write_platforms(self.plat_list)
+ self.update_plat_list()
+ self.db_indices = [x for x in range(len(self.plat_name_list))]
+
+ def toggle_platform_state(self):
+ index = self.platListWidget.currentRow()
+ state = self.plat_enabled_check.isChecked()
+ self.plat_list[index].enabled = state
+
+ def remove_platform(self):
+ index = self.platListWidget.currentRow()
+ del_dialog_res = self.del_plat_dialog.exec()
+ if del_dialog_res == QMessageBox.StandardButton.Yes:
+ self.db.remove_platform(index)
+ self.update_plat_list()
diff --git a/src/resource_finder.py b/src/resource_finder.py
index d0e006a..f2b33e2 100644
--- a/src/resource_finder.py
+++ b/src/resource_finder.py
@@ -10,4 +10,4 @@ def get_res_path(relative_path):
except AttributeError:
base_path = os.path.abspath(".")
- return os.path.join(base_path, relative_path)
\ No newline at end of file
+ return os.path.join(base_path, relative_path)
diff --git a/src/widgets/fastedit_spinbox.py b/src/widgets/fastedit_spinbox.py
index dcdc692..ae93506 100644
--- a/src/widgets/fastedit_spinbox.py
+++ b/src/widgets/fastedit_spinbox.py
@@ -7,6 +7,7 @@ class FastEditQDoubleSpinBox(QDoubleSpinBox):
QTimer.singleShot(0, self.selectAll)
super(FastEditQDoubleSpinBox, self).focusInEvent(e)
+
class FastEditQSpinBox(QSpinBox):
def focusInEvent(self, e):
QTimer.singleShot(0, self.selectAll)
diff --git a/src/widgets/mpl_widget.py b/src/widgets/mpl_widget.py
new file mode 100644
index 0000000..a5de8ac
--- /dev/null
+++ b/src/widgets/mpl_widget.py
@@ -0,0 +1,13 @@
+from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
+from matplotlib.figure import Figure
+from PyQt6.QtWidgets import QWidget, QVBoxLayout
+
+
+class MplWidget(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.canvas = FigureCanvasQTAgg(Figure(figsize=(15, 8), dpi=100))
+ vertical_layout = QVBoxLayout()
+ vertical_layout.addWidget(self.canvas)
+ self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1)
+ self.setLayout(vertical_layout)