Merge pull request #3 from RolandWH/db_testing

implement sqlite database and graphed results
This commit is contained in:
Roland W-H 2025-04-26 16:45:44 +01:00 committed by GitHub
commit ca8f4409c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1173 additions and 340 deletions

BIN
SIPPCompare.db Normal file

Binary file not shown.

View File

@ -5,7 +5,7 @@ a = Analysis(
['src\\main.py'], ['src\\main.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[('gui/*.ui', 'gui'), ('icon2.ico', '.')], datas=[('gui/*.ui', 'gui'), ('gui/dialogs/*.ui', 'gui/dialogs'), ('icon2.ico', '.')],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlatformRename</class>
<widget class="QDialog" name="PlatformRename">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>100</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>100</height>
</size>
</property>
<property name="windowTitle">
<string>Name Platform</string>
</property>
<widget class="QLineEdit" name="rename_plat_box">
<property name="geometry">
<rect>
<x>7</x>
<y>41</y>
<width>287</width>
<height>22</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
</widget>
<widget class="QLabel" name="rename_plat_lab">
<property name="geometry">
<rect>
<x>35</x>
<y>9</y>
<width>241</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Enter a new name for the platform</string>
</property>
</widget>
<widget class="QPushButton" name="rename_plat_ok_but">
<property name="geometry">
<rect>
<x>220</x>
<y>70</y>
<width>75</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>OK</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>rename_plat_ok_but</sender>
<signal>clicked()</signal>
<receiver>PlatformRename</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>257</x>
<y>71</y>
</hint>
<hint type="destinationlabel">
<x>149</x>
<y>44</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -220,13 +220,13 @@
<property name="title"> <property name="title">
<string>File</string> <string>File</string>
</property> </property>
<addaction name="actionEdit_Platforms"/> <addaction name="actionList_Platforms"/>
</widget> </widget>
<addaction name="menuPlatforms"/> <addaction name="menuPlatforms"/>
</widget> </widget>
<action name="actionEdit_Platforms"> <action name="actionList_Platforms">
<property name="text"> <property name="text">
<string>Edit Platforms</string> <string>Platform List</string>
</property> </property>
<property name="font"> <property name="font">
<font> <font>

View File

@ -1,37 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ResultsWindow</class> <class>OutputWindow</class>
<widget class="QWidget" name="ResultsWindow"> <widget class="QWidget" name="OutputWindow">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>1330</width>
<height>355</height> <height>685</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>400</width> <width>1330</width>
<height>355</height> <height>685</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>400</width> <width>1330</width>
<height>355</height> <height>685</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Results</string> <string>Results</string>
</property> </property>
<widget class="QTextEdit" name="output"> <widget class="MplWidget" name="graphWidget" native="true">
<property name="geometry">
<rect>
<x>-10</x>
<y>-10</y>
<width>1350</width>
<height>650</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="save_graph_but">
<property name="geometry">
<rect>
<x>1231</x>
<y>642</y>
<width>91</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save graph</string>
</property>
</widget>
<widget class="QSlider" name="time_slider">
<property name="geometry">
<rect>
<x>370</x>
<y>635</y>
<width>581</width>
<height>41</height>
</rect>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksAbove</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
<widget class="QPushButton" name="save_csv_but">
<property name="geometry">
<rect>
<x>1134</x>
<y>642</y>
<width>91</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save CSV</string>
</property>
</widget>
<widget class="QLabel" name="time_lab">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>10</y> <y>640</y>
<width>381</width> <width>341</width>
<height>301</height> <height>31</height>
</rect> </rect>
</property> </property>
<property name="font"> <property name="font">
@ -39,66 +110,19 @@
<pointsize>11</pointsize> <pointsize>11</pointsize>
</font> </font>
</property> </property>
</widget>
<widget class="QPushButton" name="res_ok_but">
<property name="geometry">
<rect>
<x>318</x>
<y>323</y>
<width>75</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text"> <property name="text">
<string>OK</string> <string>Fees over 1 year(s) (assuming no change in value)</string>
</property>
</widget>
<widget class="QPushButton" name="res_save_but">
<property name="geometry">
<rect>
<x>238</x>
<y>323</y>
<width>75</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save</string>
</property> </property>
</widget> </widget>
</widget> </widget>
<tabstops> <customwidgets>
<tabstop>output</tabstop> <customwidget>
<tabstop>res_save_but</tabstop> <class>MplWidget</class>
<tabstop>res_ok_but</tabstop> <extends>QWidget</extends>
</tabstops> <header>widgets/mpl_widget</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/> <resources/>
<connections> <connections/>
<connection>
<sender>res_ok_but</sender>
<signal>clicked(bool)</signal>
<receiver>ResultsWindow</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>357</x>
<y>281</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
</connections>
</ui> </ui>

View File

@ -2,6 +2,9 @@
<ui version="4.0"> <ui version="4.0">
<class>PlatformEdit</class> <class>PlatformEdit</class>
<widget class="QWidget" name="PlatformEdit"> <widget class="QWidget" name="PlatformEdit">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
@ -10,6 +13,18 @@
<height>567</height> <height>567</height>
</rect> </rect>
</property> </property>
<property name="minimumSize">
<size>
<width>630</width>
<height>567</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>630</width>
<height>567</height>
</size>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Platform Editor</string> <string>Platform Editor</string>
</property> </property>
@ -215,7 +230,7 @@
</font> </font>
</property> </property>
<property name="text"> <property name="text">
<string>Fund dealing fee*</string> <string>Fund dealing fee</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -339,7 +354,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>8</x> <x>8</x>
<y>262</y> <y>540</y>
<width>191</width> <width>191</width>
<height>21</height> <height>21</height>
</rect> </rect>
@ -505,6 +520,24 @@
<string>on the value above £ there is no charge</string> <string>on the value above £ there is no charge</string>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>284</y>
<width>151</width>
<height>16</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Fund platform fee*</string>
</property>
</widget>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

153
gui/platform_list.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlatformList</class>
<widget class="QWidget" name="PlatformList">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>263</width>
<height>473</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>263</width>
<height>473</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>263</width>
<height>473</height>
</size>
</property>
<property name="windowTitle">
<string>Platform List</string>
</property>
<widget class="QListWidget" name="platListWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>34</y>
<width>243</width>
<height>371</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
</widget>
<widget class="QPushButton" name="add_plat_but">
<property name="geometry">
<rect>
<x>11</x>
<y>413</y>
<width>121</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Add platform</string>
</property>
</widget>
<widget class="QPushButton" name="del_plat_but">
<property name="geometry">
<rect>
<x>135</x>
<y>413</y>
<width>121</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Remove platform</string>
</property>
</widget>
<widget class="QPushButton" name="plist_save_but">
<property name="geometry">
<rect>
<x>136</x>
<y>442</y>
<width>121</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
<widget class="QPushButton" name="edit_plat_but">
<property name="geometry">
<rect>
<x>11</x>
<y>442</y>
<width>121</width>
<height>24</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Edit platform</string>
</property>
</widget>
<widget class="QCheckBox" name="plat_enabled_check">
<property name="geometry">
<rect>
<x>12</x>
<y>8</y>
<width>231</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Platform enabled?</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>plist_save_but</sender>
<signal>clicked()</signal>
<receiver>PlatformList</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>196</x>
<y>453</y>
</hint>
<hint type="destinationlabel">
<x>131</x>
<y>236</y>
</hint>
</hints>
</connection>
</connections>
</ui>

16
src/data_struct.py Normal file
View File

@ -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

264
src/db_handler.py Normal file
View File

@ -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()

View File

@ -1,20 +1,11 @@
from PyQt6.QtWidgets import QApplication
import sys import sys
import platform_edit from PyQt6.QtWidgets import QApplication
from main_window import SIPPCompare
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Show platform edit window first, before main win window = SIPPCompare()
# 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.show() window.show()
app.exec() app.exec()

View File

@ -1,48 +1,41 @@
from PyQt6.QtGui import QIntValidator, QIcon
from PyQt6.QtWidgets import QMainWindow, QWidget
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtGui import QIntValidator, QIcon
from PyQt6.QtWidgets import QMainWindow, QApplication
import output_window
import resource_finder import resource_finder
from db_handler import DBHandler
from platform_list import PlatformList
from output_window import OutputWindow
class SIPPCompare(QMainWindow): class SIPPCompare(QMainWindow):
# Receive instance of PlatformEdit() as parameter def __init__(self):
def __init__(self, plat_edit_win: QWidget):
super().__init__() super().__init__()
# Import Qt Designer UI XML file # Import Qt Designer UI XML file
uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables # 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 # Results
self.fund_plat_fees = 0.0 self.fund_plat_fees = 0.0
self.fund_deal_fees = 0.0 self.fund_deal_fees = 0.0
self.share_plat_fees = 0.0 self.share_plat_fees = 0.0
self.share_deal_fees = 0.0 self.share_deal_fees = 0.0
self.results = []
# Create window objects # Create window objects
self.platform_win = plat_edit_win self.db = DBHandler()
self.output_win = output_window.OutputWindow() self.platform_list_win = PlatformList(self.db)
self.output_win = OutputWindow()
# Handle events # Handle events
self.calc_but.clicked.connect(self.calculate_fees) self.calc_but.clicked.connect(self.calculate_fees)
# Menu bar entry (File -> Edit Platforms) # Menu bar entry (File -> Platform List)
self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) self.actionList_Platforms.triggered.connect(self.show_platform_list)
# Update percentage mix label when slider moved # Update percentage mix label when slider moved
self.mix_slider.valueChanged.connect(self.update_slider_lab) self.mix_slider.valueChanged.connect(self.update_slider_lab)
self.value_input.valueChanged.connect(self.check_valid) self.value_input.valueChanged.connect(self.check_valid)
# Validate input
self.share_trades_combo.currentTextChanged.connect(self.check_valid) self.share_trades_combo.currentTextChanged.connect(self.check_valid)
self.fund_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.share_trades_combo.setValidator(QIntValidator(0, 999))
self.fund_trades_combo.setValidator(QIntValidator(0, 99)) 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) # Display slider position as mix between two nums (funds/shares)
def update_slider_lab(self): def update_slider_lab(self):
slider_val = self.mix_slider.value() slider_val = self.mix_slider.value()
mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)" mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)"
self.mix_lab.setText(mix_lab_str) self.mix_lab.setText(mix_lab_str)
# Ensure that trade fields aren't blank and pension value > 0
def check_valid(self): def check_valid(self):
if self.share_trades_combo.currentText() != "" \ if self.share_trades_combo.currentText() != "" \
and self.fund_trades_combo.currentText() != "" \ and self.fund_trades_combo.currentText() != "" \
@ -64,90 +67,71 @@ class SIPPCompare(QMainWindow):
else: else:
self.calc_but.setEnabled(False) self.calc_but.setEnabled(False)
# Get variables from platform editor input fields # Calculate fees for all active platforms
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
def calculate_fees(self): def calculate_fees(self):
self.init_variables() # Set to empty list each time to avoid persistence
# Set to zero each time to avoid persistence self.results = []
self.fund_plat_fees = 0
# Get user input
value_num = float(self.value_input.value()) value_num = float(self.value_input.value())
# Funds/shares mix
slider_val: int = self.mix_slider.value() slider_val: int = self.mix_slider.value()
funds_value = (slider_val / 100) * value_num
fund_trades_num = int(self.fund_trades_combo.currentText()) 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()) share_trades_num = int(self.share_trades_combo.currentText())
if self.share_deal_reduce_trades is not None: shares_value = (1 - (slider_val / 100)) * value_num
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
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() self.show_output_win()
# Show the output window - this func is called from calculate_fee() # Show the output window - this func is called from calculate_fee()
def show_output_win(self): def show_output_win(self):
# Refresh the results when new fees are calculated # Refresh the results when new fees are calculated
self.output_win.display_output(self.fund_plat_fees, self.fund_deal_fees, self.output_win.display_output(self.results, 1)
self.share_plat_fees, self.share_deal_fees, self.output_win.activateWindow()
self.plat_name self.output_win.raise_()
)
self.output_win.show() self.output_win.show()
QApplication.alert(self.output_win)
# Show the platform editor window (currently run-time only) def show_platform_list(self):
def show_platform_edit(self): self.platform_list_win.show()
self.platform_win.show()

View File

@ -1,11 +1,32 @@
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QWidget
from PyQt6 import uic
import datetime
import os 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 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): class OutputWindow(QWidget):
@ -15,50 +36,103 @@ class OutputWindow(QWidget):
uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables # Define class variables
self.results_str = "" self.save_err_dialog = SaveFailure()
self.platform_name = "" self.canvas = self.graphWidget.canvas
self.ax = self.canvas.axes
self.fig = self.canvas.figure
self.results = []
# Handle events # 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): def display_output(self, results: list, years: int):
# Use datatime for txt filename self.results = results
cur_time = datetime.datetime.now() self.ax.clear()
if not os.path.exists("output"): self.ax.cla()
os.makedirs("output") self.canvas.draw_idle()
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)
# Display fees in output window as plaintext readout names = []
def display_output(self, fund_plat_fees: float, fund_deal_fees: float, values = []
share_plat_fees: float, share_deal_fees: float, plat_name: str): for result in results:
self.platform_name = plat_name names.append(result[4])
if self.platform_name is not None: values.append(sum(result[:4]) * years)
self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):"
else:
self.results_str = f"Fees breakdown:"
self.results_str += "\n\nPlatform fees:" names = sorted(names, key=lambda x: values[names.index(x)], reverse=True)
# :.2f is used in order to display 2 decimal places (currency form) values = sorted(values, reverse=True)
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}"
self.results_str += "\n\nDealing fees:" h_bars = self.ax.barh(names, values)
self.results_str += f"\n\tFund dealing fees: £{round(fund_deal_fees, 2):.2f}" self.ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues])
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}"
total_fees = total_plat_fees + total_deal_fees def save_graph(self):
self.results_str += f"\n\nTotal fees: £{round(total_fees, 2):.2f}" 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)

View File

@ -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.QtGui import QRegularExpressionValidator, QFont, QIcon
from PyQt6.QtWidgets import QWidget, QLabel from PyQt6.QtWidgets import QWidget, QLabel
from PyQt6 import uic
from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
import main_window
import resource_finder import resource_finder
from db_handler import DBHandler
from data_struct import Platform
from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
class PlatformEdit(QWidget): class PlatformEdit(QWidget):
def __init__(self, autofill: bool): def __init__(self, plat: Platform):
super().__init__() super().__init__()
# Import Qt Designer UI XML file # Import Qt Designer UI XML file
uic.loadUi(resource_finder.get_res_path("gui/platform_edit.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/platform_edit.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables # Initialise class variables
# Create main window object, passing this instance as param self.plat = plat
self.main_win = main_window.SIPPCompare(self) self.fund_plat_fee = self.plat.fund_plat_fee
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.widgets_list_list = [] self.widgets_list_list = []
if len(self.plat.fund_plat_fee[0]) > 1:
self.fund_fee_rows = 1 self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - 1
# Debugging feature: set with "--DEBUG_AUTOFILL" cmd argument else:
self.autofill = autofill self.fund_fee_rows = 1
if autofill:
self.save_but.setEnabled(True)
self.required_fields = [ self.required_fields = [
self.share_plat_fee_box, self.share_plat_fee_box,
@ -64,6 +54,57 @@ class PlatformEdit(QWidget):
False 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 # Handle events
for field in self.required_fields: for field in self.required_fields:
field.valueChanged.connect(self.check_valid) field.valueChanged.connect(self.check_valid)
@ -93,7 +134,7 @@ class PlatformEdit(QWidget):
QRegularExpressionValidator(QRegularExpression("\\w*")) 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], [0]]
plat_fee_struct[0].append(self.first_tier_box.value()) plat_fee_struct[0].append(self.first_tier_box.value())
plat_fee_struct[1].append(self.first_tier_fee_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 # Get fee structure variables from user input when "Save" clicked
def init_variables(self): def init_variables(self):
# If debugging, save time by hardcoding self.plat.fund_plat_fee = self.create_plat_fee_struct()
if self.autofill: self.plat.share_plat_fee = float(self.share_plat_fee_box.value()) / 100
self.plat_name = "AJBell" self.plat.share_deal_fee = float(self.share_deal_fee_box.value())
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())
# Once user input is received show main window if self.check_boxes_ticked[0]:
self.main_win.show() 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: # This method does multiple things in order to validate the user's inputs:
# 1) Check all required fields have a non-zero value # 1) Check all required fields have a non-zero value
@ -215,50 +258,67 @@ class PlatformEdit(QWidget):
max_band = self.first_tier_box.value() max_band = self.first_tier_box.value()
self.val_above_lab.setText(f"on the value above £{int(max_band)} there is no charge") self.val_above_lab.setText(f"on the value above £{int(max_band)} there is no charge")
def add_row(self): def add_row(self, loading: bool = False):
widgets = [] if loading:
font = QFont() rows_needed = self.fund_fee_rows - 1
font.setPointSize(11) else:
rows_needed = 1
widgets.append(QLabel(self.gridLayoutWidget_2)) for x in range(rows_needed):
widgets[0].setFont(font) widgets = []
font = QFont()
font.setPointSize(11)
widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) widgets.append(QLabel(self.gridLayoutWidget_2))
widgets[1].setPrefix("£") widgets[0].setFont(font)
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.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
widgets[2].setText(f"the fee is") widgets[1].setPrefix("£")
widgets[2].setFont(font) 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.append(QLabel(self.gridLayoutWidget_2))
widgets[3].setSuffix("%") widgets[2].setText(f"the fee is")
widgets[3].setMaximum(100) widgets[2].setFont(font)
widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons)
widgets[3].setFont(font)
widgets[3].valueChanged.connect(self.check_valid)
# TODO: why 28.5? widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2))
self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0))) widgets[3].setSuffix("%")
for i in range(len(widgets)): widgets[3].setMaximum(100)
self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) 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) if not loading:
cur_label_idx = self.gridLayout_2.indexOf(widgets[0]) self.fund_fee_rows += 1
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 self.widgets_list_list.append(widgets)
prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() cur_label_idx = self.gridLayout_2.indexOf(widgets[0])
cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() cur_box_idx = self.gridLayout_2.indexOf(widgets[1])
cur_label_item.setText(f"between £{int(prev_box_item.value())} and") 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: if self.fund_fee_rows > 1:
self.del_row_but.setEnabled(True) self.del_row_but.setEnabled(True)
@ -267,8 +327,9 @@ class PlatformEdit(QWidget):
self.add_row_but.setEnabled(False) self.add_row_but.setEnabled(False)
self.check_valid() self.check_valid()
self.update_tier_labels()
# TODO: Tab order # TODO: Tab/focus order
def remove_row(self): def remove_row(self):
for widget in self.widgets_list_list[self.fund_fee_rows - 2]: for widget in self.widgets_list_list[self.fund_fee_rows - 2]:
@ -286,31 +347,3 @@ class PlatformEdit(QWidget):
self.check_valid() self.check_valid()
self.update_tier_labels() 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

148
src/platform_list.py Normal file
View File

@ -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()

View File

@ -7,6 +7,7 @@ class FastEditQDoubleSpinBox(QDoubleSpinBox):
QTimer.singleShot(0, self.selectAll) QTimer.singleShot(0, self.selectAll)
super(FastEditQDoubleSpinBox, self).focusInEvent(e) super(FastEditQDoubleSpinBox, self).focusInEvent(e)
class FastEditQSpinBox(QSpinBox): class FastEditQSpinBox(QSpinBox):
def focusInEvent(self, e): def focusInEvent(self, e):
QTimer.singleShot(0, self.selectAll) QTimer.singleShot(0, self.selectAll)

13
src/widgets/mpl_widget.py Normal file
View File

@ -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)