| 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): |
| def __init__(self, parent, label_text): |
| super().__init__(parent) |
| 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() |
| |
| parsed_date = datetime.strptime(date_str, '%Y-%m-%d').date() |
| |
| 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") |
| |
| 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') |
| |
| |
| try: |
| current = datetime.strptime(self.selected_date.get(), '%Y-%m-%d').date() |
| except ValueError: |
| current = date.today() |
| |
| |
| cal = Calendar(self.top, selectmode='day', |
| year=current.year, |
| month=current.month, |
| day=current.day, |
| date_pattern='y-mm-dd') |
| 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") |
| |
| |
| 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() |
| |
| |
| self.notebook = ttk.Notebook(root) |
| self.notebook.pack(fill='both', expand=True, padx=10, pady=5) |
| |
| |
| 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): |
| |
| input_frame = ttk.LabelFrame(self.single_bond_frame, text="Bond Details") |
| input_frame.pack(fill='x', padx=20, pady=10, expand=False) |
| |
| |
| self.settlement_date = DateSelector(input_frame, "Settlement Date:") |
| self.settlement_date.pack(padx=10, pady=5, anchor='w') |
|
|
| |
| self.maturity_date = DateSelector(input_frame, "Maturity Date: ") |
| self.maturity_date.pack(padx=10, pady=5, anchor='w') |
| |
| |
| rates_frame = ttk.Frame(input_frame) |
| rates_frame.pack(fill='x', padx=10, pady=5) |
| |
| |
| 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_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) |
| |
| |
| vcmd = (self.root.register(self._validate_float), '%P') |
| self.coupon_rate.config(validate='key', validatecommand=vcmd) |
| self.market_rate.config(validate='key', validatecommand=vcmd) |
| |
| |
| 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) |
| |
| |
| self.results_frame = ttk.LabelFrame(self.single_bond_frame, text="Results") |
| self.results_frame.pack(fill='x', padx=20, pady=10, expand=False) |
| |
| |
| 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): |
| |
| file_frame = ttk.Frame(self.batch_calculation_frame) |
| file_frame.pack(fill='x', padx=20, pady=10) |
| |
| |
| 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) |
| |
| |
| table_frame = ttk.Frame(self.batch_calculation_frame) |
| table_frame.pack(fill='both', expand=True, padx=20, pady=10) |
| |
| |
| scrollbar = ttk.Scrollbar(table_frame) |
| scrollbar.pack(side=tk.RIGHT, fill=tk.Y) |
| |
| |
| self.tree = ttk.Treeview(table_frame, |
| columns=("ISIN", "Maturity", "Coupon", "Yield", "Clean Price"), |
| show='headings', |
| yscrollcommand=scrollbar.set) |
| |
| |
| 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") |
| |
| |
| 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) |
| |
| |
| self.tree.pack(side=tk.LEFT, fill='both', expand=True) |
| scrollbar.config(command=self.tree.yview) |
|
|
| def _calculate_single_bond(self): |
| try: |
| |
| settlement = self.settlement_date.get_date() |
| maturity = self.maturity_date.get_date() |
| |
| |
| if maturity <= settlement: |
| raise ValueError("Maturity date must be after settlement date") |
| |
| |
| 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 |
| market = float(self.market_rate.get()) / 100 |
| |
| |
| if not (0 <= coupon <= 1) or not (0 <= market <= 1): |
| raise ValueError("Rates must be between 0 and 100%") |
| |
| |
| 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): |
| |
| 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 |
| |
| |
| 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() |
|
|