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('', self._validate_date_entry) self.date_entry.bind('', 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()