Spaces:
Running
Running
fix: add environment-aware logging with prod/dev/test levels and fix CSS not applying in Gradio 6
e60df7c | """ | |
| app.py -- Gradio UI for Value at Risk Analysis. | |
| """ | |
| import os | |
| import pandas as pd | |
| import gradio as gr | |
| from src.logger import logger | |
| from src.config import TICKERS, LOOKBACK_DAYS, STRESS_START_DATE, STRESS_END_DATE, STRESS_LABEL | |
| from src.historical import historical_var_es_pipeline | |
| from src.parametric import parametric_var_es_pipeline | |
| def calculate_var_analysis( | |
| ticker: str, | |
| end_date_str: str, | |
| portfolio_value: float, | |
| n_days: int, | |
| var_confidence_label: str, | |
| es_confidence_label: str, | |
| method: str, | |
| ): | |
| """Calculate Value at Risk analysis based on Gradio inputs and delegate to the analysis pipeline.""" | |
| logger.debug( | |
| f"Analysis requested: {ticker} | VaR={var_confidence_label} ES={es_confidence_label} | {method} | N={n_days} | Date={end_date_str} | PV=${portfolio_value:,.0f}" | |
| ) | |
| var_confidence = float(var_confidence_label.strip().replace("%", "")) / 100.0 | |
| es_confidence = float(es_confidence_label.strip().replace("%", "")) / 100.0 | |
| var_conf_pct = var_confidence_label.strip() | |
| es_conf_pct = es_confidence_label.strip() | |
| today = pd.Timestamp.today().normalize() | |
| try: | |
| end_date = pd.to_datetime(end_date_str, errors="raise").normalize() | |
| except Exception: | |
| gr.Warning("Invalid date selection. Please try again.") | |
| return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method) | |
| if end_date >= today: | |
| gr.Warning( | |
| "Invalid date selection. VaR estimation requires historical data, so please choose a date prior to today." | |
| ) | |
| return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method) | |
| fy_start = pd.Timestamp(year=today.year - 1, month=4, day=1) | |
| if end_date < fy_start: | |
| gr.Warning(f"VaR Date must be after {fy_start.strftime('%Y-%m-%d')}") | |
| return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method) | |
| if portfolio_value <= 0: | |
| gr.Warning("Portfolio value must be positive.") | |
| return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method) | |
| pipeline = historical_var_es_pipeline if method == "Historical VaR" else parametric_var_es_pipeline | |
| result = pipeline( | |
| ticker, | |
| var_confidence, | |
| es_confidence, | |
| LOOKBACK_DAYS, | |
| int(n_days), | |
| portfolio_value, | |
| end_date, | |
| stress_start=STRESS_START_DATE, | |
| stress_end=STRESS_END_DATE, | |
| stress_label=STRESS_LABEL, | |
| ) | |
| logger.info( | |
| f"Analysis complete: {ticker} | VaR=${result['var_nd']:,.2f} | ES=${result['es_nd']:,.2f}" | |
| ) | |
| return ( | |
| gr.update( | |
| label=f"{int(n_days)}-day {var_conf_pct} VaR", | |
| value=f"${result['var_nd']:,.2f}", | |
| ), | |
| gr.update( | |
| label=f"{int(n_days)}-day {es_conf_pct} ES", | |
| value=f"${result['es_nd']:,.2f}", | |
| ), | |
| gr.update( | |
| label=f"{int(n_days)}-day {var_conf_pct} Stressed VaR", | |
| value=f"${result['stressed_var_nd']:,.2f}", | |
| ), | |
| gr.update( | |
| label=f"{int(n_days)}-day {es_conf_pct} Stressed ES", | |
| value=f"${result['stressed_es_nd']:,.2f}", | |
| ), | |
| gr.update(value=result["fig_dist"], visible=True), | |
| gr.update(value=result["excel_path"], visible=True), | |
| ) | |
| def reset_analysis_results(n_days: float, var_confidence_label: str, es_confidence_label: str, method: str): | |
| """Reset and hide analysis results when input parameters are modified.""" | |
| var_conf_pct = var_confidence_label.strip() | |
| es_conf_pct = es_confidence_label.strip() | |
| method_short = "Historical" if method == "Historical VaR" else "Parametric" | |
| return ( | |
| gr.update(value="", label=f"{int(n_days)}-day {var_conf_pct} VaR"), | |
| gr.update(value="", label=f"{int(n_days)}-day {es_conf_pct} ES"), | |
| gr.update(value="", label=f"{int(n_days)}-day {var_conf_pct} Stressed VaR"), | |
| gr.update(value="", label=f"{int(n_days)}-day {es_conf_pct} Stressed ES"), | |
| gr.update(value=None, visible=False), | |
| gr.update(visible=False), | |
| ) | |
| def enable_run_button_for_method(method: str): | |
| """Enable the Run Analysis button only for fully implemented VaR methods.""" | |
| return gr.update(interactive=(method in ("Historical VaR", "Parametric VaR"))) | |
| # ------------------------------------------------------------------ | |
| # UI builder | |
| # ------------------------------------------------------------------ | |
| CUSTOM_CSS = """ | |
| .form { border: none !important; box-shadow: none !important; gap: 0 !important; } | |
| .form .block, .form .row, .form > * { border: none !important; box-shadow: none !important; } | |
| #excel-btn, #excel-btn.primary { | |
| background: #f97316 !important; | |
| background-color: #f97316 !important; | |
| color: white !important; | |
| border-color: #9a3412 !important; | |
| border: 1px solid #9a3412 !important; | |
| } | |
| #excel-btn:hover, #excel-btn.primary:hover { | |
| background: #ea580c !important; | |
| background-color: #ea580c !important; | |
| border-color: #9a3412 !important; | |
| border: 1px solid #9a3412 !important; | |
| } | |
| #portfolio-hr { | |
| margin: 2rem 0 0.5rem !important; | |
| border: none !important; | |
| border-top: 1px solid #e5e7eb !important; | |
| } | |
| #portfolio-footer { | |
| width: 100% !important; | |
| max-width: 100% !important; | |
| } | |
| #portfolio-text { | |
| text-align: center !important; | |
| margin: 0 auto !important; | |
| padding: 0.75rem 0 !important; | |
| width: 100% !important; | |
| } | |
| #portfolio-text a { | |
| color: #ffffff !important; | |
| font-weight: 500 !important; | |
| font-size: 0.8rem !important; | |
| font-family: 'Inter', 'Segoe UI', system-ui, sans-serif !important; | |
| letter-spacing: 0.05em !important; | |
| text-decoration: none !important; | |
| cursor: pointer !important; | |
| } | |
| #portfolio-text a:hover { | |
| color: #d1d5db !important; | |
| } | |
| """ | |
| def build_app() -> gr.Blocks: | |
| """Construct and return the Gradio Blocks application.""" | |
| with gr.Blocks( | |
| title="VaR Engine", | |
| ) as app: | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| gr.Markdown("# VaR Engine") | |
| with gr.Column(scale=1, min_width=260): | |
| download_file = gr.DownloadButton( | |
| "Download Excel Report", | |
| variant="primary", | |
| visible=False, | |
| elem_id="excel-btn", | |
| ) | |
| with gr.Row(): | |
| # Sidebar | |
| with gr.Column(scale=1, min_width=260): | |
| gr.Markdown("### Inputs") | |
| with gr.Group(): | |
| ticker_dd = gr.Dropdown( | |
| choices=TICKERS, | |
| value=TICKERS[0], | |
| label="Ticker", | |
| ) | |
| end_date_input = gr.DateTime( | |
| include_time=False, | |
| type="datetime", | |
| label="VaR Date", | |
| ) | |
| portfolio_val_input = gr.Number( | |
| value=1000000, | |
| label="Portfolio Value ($)", | |
| ) | |
| n_days_slider = gr.Slider( | |
| minimum=1, | |
| maximum=15, | |
| step=1, | |
| value=10, | |
| label="N Days for VaR", | |
| ) | |
| with gr.Row(): | |
| confidence_dd = gr.Dropdown( | |
| choices=["99%", "97.5%", "95%"], | |
| value="99%", | |
| label="VaR Confidence", | |
| min_width=100, | |
| ) | |
| es_confidence_dd = gr.Dropdown( | |
| choices=["99%", "97.5%", "95%"], | |
| value="99%", | |
| label="ES Confidence", | |
| min_width=100, | |
| ) | |
| method_radio = gr.Radio( | |
| choices=[ | |
| "Historical VaR", | |
| "Parametric VaR", | |
| ], | |
| value="Historical VaR", | |
| label="Method", | |
| ) | |
| run_btn = gr.Button("Run Analysis", variant="primary") | |
| # Enable/disable run button based on method availability | |
| method_radio.change( | |
| fn=enable_run_button_for_method, | |
| inputs=method_radio, | |
| outputs=run_btn, | |
| ) | |
| with gr.Column(scale=3): | |
| gr.Markdown("### Results") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| var_box = gr.Textbox( | |
| label="10-day 99% VaR", | |
| interactive=False, | |
| ) | |
| with gr.Column(scale=1): | |
| es_box = gr.Textbox( | |
| label="10-day 99% ES", | |
| interactive=False, | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| stressed_var_box = gr.Textbox( | |
| label="10-day 99% Stressed VaR", | |
| interactive=False, | |
| ) | |
| with gr.Column(scale=1): | |
| stressed_es_box = gr.Textbox( | |
| label="10-day 99% Stressed ES", | |
| interactive=False, | |
| ) | |
| plot_dist = gr.Plot(show_label=False, visible=False) | |
| # Wiring | |
| all_outputs = [var_box, es_box, stressed_var_box, stressed_es_box, plot_dist, download_file] | |
| run_btn.click( | |
| fn=calculate_var_analysis, | |
| inputs=[ | |
| ticker_dd, | |
| end_date_input, | |
| portfolio_val_input, | |
| n_days_slider, | |
| confidence_dd, | |
| es_confidence_dd, | |
| method_radio, | |
| ], | |
| outputs=all_outputs, | |
| ) | |
| all_inputs = [ | |
| ticker_dd, | |
| end_date_input, | |
| portfolio_val_input, | |
| n_days_slider, | |
| confidence_dd, | |
| es_confidence_dd, | |
| method_radio, | |
| ] | |
| label_inputs = [n_days_slider, confidence_dd, es_confidence_dd, method_radio] | |
| for comp in all_inputs: | |
| if comp is n_days_slider: | |
| comp.release( | |
| fn=reset_analysis_results, | |
| inputs=label_inputs, | |
| outputs=all_outputs, | |
| ) | |
| else: | |
| comp.change( | |
| fn=reset_analysis_results, | |
| inputs=label_inputs, | |
| outputs=all_outputs, | |
| ) | |
| method_radio.change( | |
| fn=enable_run_button_for_method, inputs=method_radio, outputs=run_btn | |
| ) | |
| gr.HTML( | |
| '<hr id="portfolio-hr">' | |
| '<p id="portfolio-text">' | |
| '<a href="https://kameshcodes.github.io/portfolio/" target="_blank">' | |
| "\u00a9 kameshcodes" | |
| "</a></p>", | |
| elem_id="portfolio-footer", | |
| ) | |
| return app | |
| # ------------------------------------------------------------------ | |
| # Entry point | |
| # ------------------------------------------------------------------ | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| application = build_app() | |
| application.launch(server_name="0.0.0.0", server_port=port, share=False, theme=gr.themes.Base(), css=CUSTOM_CSS) | |