ZenoEconomicus
Add Tkinter bond calculator GUI
3bd219d
Raw
History Blame Contribute Delete
13.8 kB
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from tkcalendar import Calendar
from datetime import datetime, date
import pandas as pd
from bond_calculator import AustralianTreasuryBondCalculator
from data_loader import DataLoader
class DateSelector(ttk.Frame): # <-- Use ttk.Frame here
def __init__(self, parent, label_text):
super().__init__(parent) # <-- Remove borderwidth and highlightthickness
self.selected_date = tk.StringVar()
self.selected_date.set(date.today().strftime('%Y-%m-%d'))
ttk.Label(self, text=label_text).grid(row=0, column=0, padx=5, pady=2, sticky='w')
self.date_entry = ttk.Entry(self, textvariable=self.selected_date, width=12)
self.date_entry.grid(row=0, column=1, padx=5, pady=2)
self.date_entry.bind('<FocusOut>', self._validate_date_entry)
self.date_entry.bind('<Return>', self._validate_date_entry)
self.cal_button = ttk.Button(self, text="Cal", width=4, command=self.show_calendar)
self.cal_button.grid(row=0, column=2, padx=5, pady=2)
self.top = None
def _validate_date_entry(self, event=None):
"""Validate date when user finishes editing"""
try:
date_str = self.selected_date.get()
# Try to parse the date
parsed_date = datetime.strptime(date_str, '%Y-%m-%d').date()
# If successful, format it consistently
self.selected_date.set(parsed_date.strftime('%Y-%m-%d'))
except ValueError:
messagebox.showerror("Invalid Date",
"Please enter date in YYYY-MM-DD format\nExample: 2025-08-28")
# Reset to today's date
self.selected_date.set(date.today().strftime('%Y-%m-%d'))
return False
return True
def show_calendar(self):
if self.top:
return
self.top = tk.Toplevel(self)
self.top.geometry('300x300')
self.top.title('Select Date')
# Parse current date
try:
current = datetime.strptime(self.selected_date.get(), '%Y-%m-%d').date()
except ValueError:
current = date.today()
# Create calendar with date pattern
cal = Calendar(self.top, selectmode='day',
year=current.year,
month=current.month,
day=current.day,
date_pattern='y-mm-dd') # Set the date pattern to match required format
cal.pack(fill="both", expand=True)
def set_date():
selected = cal.selection_get()
self.selected_date.set(selected.strftime('%Y-%m-%d'))
self.top.destroy()
self.top = None
ttk.Button(self.top, text="Select", command=set_date).pack(pady=10)
def get_date(self):
"""Get the selected date, ensuring it's in the correct format"""
if not self._validate_date_entry():
raise ValueError("Invalid date format. Use YYYY-MM-DD")
return datetime.strptime(self.selected_date.get(), '%Y-%m-%d').date()
class BondCalculatorGUI:
def __init__(self, root):
self.root = root
self.root.title("Australian Treasury Bond Calculator")
self.root.geometry("900x700")
# Configure style
style = ttk.Style()
style.configure('TButton', padding=5)
style.configure('TLabel', padding=3)
style.configure('TEntry', padding=3)
self.calculator = AustralianTreasuryBondCalculator()
self.data_loader = DataLoader()
# Create main notebook for tabs
self.notebook = ttk.Notebook(root)
self.notebook.pack(fill='both', expand=True, padx=10, pady=5)
# Create tabs
self.single_bond_frame = ttk.Frame(self.notebook)
self.batch_calculation_frame = ttk.Frame(self.notebook)
self.notebook.add(self.single_bond_frame, text='Single Bond')
self.notebook.add(self.batch_calculation_frame, text='Batch Calculation')
self._setup_single_bond_tab()
self._setup_batch_calculation_tab()
def _setup_single_bond_tab(self):
# Create a frame for input fields
input_frame = ttk.LabelFrame(self.single_bond_frame, text="Bond Details")
input_frame.pack(fill='x', padx=20, pady=10, expand=False)
# Settlement Date
self.settlement_date = DateSelector(input_frame, "Settlement Date:")
self.settlement_date.pack(padx=10, pady=5, anchor='w')
# Maturity Date
self.maturity_date = DateSelector(input_frame, "Maturity Date: ")
self.maturity_date.pack(padx=10, pady=5, anchor='w')
# Create frame for rates
rates_frame = ttk.Frame(input_frame)
rates_frame.pack(fill='x', padx=10, pady=5)
# Coupon Rate
rate_frame = ttk.Frame(rates_frame)
rate_frame.pack(fill='x', pady=5)
ttk.Label(rate_frame, text="Coupon Rate (%):").pack(side=tk.LEFT)
self.coupon_rate = ttk.Entry(rate_frame, width=15)
self.coupon_rate.pack(side=tk.LEFT, padx=0)
# Market Rate
market_frame = ttk.Frame(rates_frame)
market_frame.pack(fill='x', pady=5)
ttk.Label(market_frame, text="Market Rate (%):").pack(side=tk.LEFT)
self.market_rate = ttk.Entry(market_frame, width=15)
self.market_rate.pack(side=tk.LEFT, padx=5)
# Add validation
vcmd = (self.root.register(self._validate_float), '%P')
self.coupon_rate.config(validate='key', validatecommand=vcmd)
self.market_rate.config(validate='key', validatecommand=vcmd)
# Calculate Button
button_frame = ttk.Frame(input_frame)
button_frame.pack(fill='x', padx=10, pady=15)
calculate_btn = ttk.Button(button_frame, text="Calculate",
command=self._calculate_single_bond)
calculate_btn.pack(expand=True)
# Results Frame
self.results_frame = ttk.LabelFrame(self.single_bond_frame, text="Results")
self.results_frame.pack(fill='x', padx=20, pady=10, expand=False)
# Results display
self.clean_price_var = tk.StringVar(value="Clean Price: --")
self.dirty_price_var = tk.StringVar(value="Dirty Price: --")
self.clean_price_label = ttk.Label(self.results_frame,
textvariable=self.clean_price_var,
font=('TkDefaultFont', 12))
self.clean_price_label.pack(pady=5)
self.dirty_price_label = ttk.Label(self.results_frame,
textvariable=self.dirty_price_var,
font=('TkDefaultFont', 12))
self.dirty_price_label.pack(pady=5)
def _validate_float(self, value):
if value == "":
return True
try:
float(value)
return True
except ValueError:
return False
def _setup_batch_calculation_tab(self):
# Top frame for file selection
file_frame = ttk.Frame(self.batch_calculation_frame)
file_frame.pack(fill='x', padx=20, pady=10)
# File Selection button and label in same frame
ttk.Button(file_frame, text="Select CSV File",
command=self._load_csv).pack(side=tk.LEFT, padx=5)
self.file_label = ttk.Label(file_frame, text="No file selected")
self.file_label.pack(side=tk.LEFT, padx=5)
# Frame for the table
table_frame = ttk.Frame(self.batch_calculation_frame)
table_frame.pack(fill='both', expand=True, padx=20, pady=10)
# Scrollbar for the table
scrollbar = ttk.Scrollbar(table_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Results Table with more columns
self.tree = ttk.Treeview(table_frame,
columns=("ISIN", "Maturity", "Coupon", "Yield", "Clean Price"),
show='headings',
yscrollcommand=scrollbar.set)
# Configure columns
self.tree.heading("ISIN", text="ISIN")
self.tree.heading("Maturity", text="Maturity Date")
self.tree.heading("Coupon", text="Coupon Rate")
self.tree.heading("Yield", text="Market Yield")
self.tree.heading("Clean Price", text="Clean Price")
# Set column widths
self.tree.column("ISIN", width=120)
self.tree.column("Maturity", width=100)
self.tree.column("Coupon", width=100)
self.tree.column("Yield", width=100)
self.tree.column("Clean Price", width=100)
# Pack the tree and configure the scrollbar
self.tree.pack(side=tk.LEFT, fill='both', expand=True)
scrollbar.config(command=self.tree.yview)
def _calculate_single_bond(self):
try:
# Get dates
settlement = self.settlement_date.get_date()
maturity = self.maturity_date.get_date()
# Validate dates
if maturity <= settlement:
raise ValueError("Maturity date must be after settlement date")
# Get and validate rates
if not self.coupon_rate.get() or not self.market_rate.get():
raise ValueError("Please enter both coupon rate and market rate")
coupon = float(self.coupon_rate.get()) / 100 # Convert percentage to decimal
market = float(self.market_rate.get()) / 100 # Convert percentage to decimal
# Validate rate ranges
if not (0 <= coupon <= 1) or not (0 <= market <= 1):
raise ValueError("Rates must be between 0 and 100%")
# Calculate clean price
clean_price = self.calculator.calculate_clean_price(
settlement,
maturity,
coupon,
market
)
dirty_price = self.calculator.calculate_dirty_price(
settlement,
maturity,
coupon,
market
)
self.clean_price_var.set(f"Clean Price: {clean_price:.4f}")
self.dirty_price_var.set(f"Dirty Price: {dirty_price:.4f}")
except ValueError as e:
messagebox.showerror("Input Error", str(e))
except Exception as e:
messagebox.showerror("Calculation Error", f"An error occurred: {str(e)}")
def _load_csv(self):
filename = filedialog.askopenfilename(
title="Select CSV file",
filetypes=[("CSV files", "*.csv")]
)
if filename:
self.file_label.config(text=filename.split("/")[-1])
try:
df = self.data_loader.load_bond_data(filename)
validation_errors = self.data_loader.validate_data(df)
if validation_errors:
error_msg = "\n".join([
f"Row {err['row']}: {err['error']} in {err['column']}"
for err in validation_errors[:5]
])
if len(validation_errors) > 5:
error_msg += f"\n... and {len(validation_errors) - 5} more errors"
messagebox.showwarning("Validation Warnings", error_msg)
self._process_batch_calculation(df)
except Exception as e:
messagebox.showerror("Error", str(e))
def _process_batch_calculation(self, df):
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
settlement_date = datetime.now().date()
for _, row in df.iterrows():
try:
maturity_date = pd.to_datetime(row['Maturity_Date']).date()
coupon_rate = float(row['Coupon_Rate']) / 100
market_yield = float(row['Market_Yield']) / 100
# Validate inputs
if maturity_date <= settlement_date:
raise ValueError("Maturity date must be after settlement date")
if not (0 <= coupon_rate <= 1) or not (0 <= market_yield <= 1):
raise ValueError("Rates must be between 0 and 100%")
clean_price = self.calculator.calculate_clean_price(
settlement_date,
maturity_date,
coupon_rate,
market_yield
)
self.tree.insert('', 'end', values=(
row['ISIN'],
maturity_date.strftime('%Y-%m-%d'),
f"{row['Coupon_Rate']:.2f}%",
f"{row['Market_Yield']:.2f}%",
f"{clean_price:.4f}"
))
except Exception as e:
self.tree.insert('', 'end', values=(
row['ISIN'],
"Error",
"",
"",
str(e)
))
def _on_closing(self):
"""Handle window closing"""
if messagebox.askokcancel("Quit", "Do you want to quit?"):
self.root.quit()
def main():
root = tk.Tk()
app = BondCalculatorGUI(root)
root.protocol("WM_DELETE_WINDOW", app._on_closing)
root.mainloop()
if __name__ == "__main__":
main()