kameshcodes commited on
Commit
e60df7c
·
1 Parent(s): dc80ba0

fix: add environment-aware logging with prod/dev/test levels and fix CSS not applying in Gradio 6

Browse files
Files changed (8) hide show
  1. app.py +55 -38
  2. config.yaml +8 -1
  3. src/config.py +2 -0
  4. src/excel_export.py +1 -1
  5. src/historical.py +2 -2
  6. src/logger.py +19 -4
  7. src/parametric.py +2 -2
  8. src/utils.py +3 -3
app.py CHANGED
@@ -22,7 +22,7 @@ def calculate_var_analysis(
22
  method: str,
23
  ):
24
  """Calculate Value at Risk analysis based on Gradio inputs and delegate to the analysis pipeline."""
25
- logger.info(
26
  f"Analysis requested: {ticker} | VaR={var_confidence_label} ES={es_confidence_label} | {method} | N={n_days} | Date={end_date_str} | PV=${portfolio_value:,.0f}"
27
  )
28
 
@@ -69,8 +69,8 @@ def calculate_var_analysis(
69
  stress_label=STRESS_LABEL,
70
  )
71
 
72
- logger.success(
73
- f"Analysis complete: VaR=${result['var_nd']:,.2f}, ES=${result['es_nd']:,.2f}, Excel={result['excel_path']}\n"
74
  )
75
 
76
  return (
@@ -121,6 +121,52 @@ def enable_run_button_for_method(method: str):
121
  # ------------------------------------------------------------------
122
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def build_app() -> gr.Blocks:
125
  """Construct and return the Gradio Blocks application."""
126
 
@@ -267,13 +313,11 @@ def build_app() -> gr.Blocks:
267
  fn=enable_run_button_for_method, inputs=method_radio, outputs=run_btn
268
  )
269
 
270
- gr.Markdown(
271
- '<hr style="margin:2rem 0 0.5rem; border:none; border-top:1px solid #e5e7eb;">'
272
- '<p style="text-align:center; margin:0; padding:0.75rem 0;">'
273
- '<a href="https://kameshcodes.github.io/portfolio/" target="_blank" '
274
- 'style="color:#fa8529; font-weight:600; font-size:1rem; text-decoration:none;">'
275
- "Built by Kamesh : Portfolio "
276
- '<span style="display:inline-block; transform:rotate(-65deg); color:#fa8529;">\u2192</span>'
277
  "</a></p>",
278
  elem_id="portfolio-footer",
279
  )
@@ -286,34 +330,7 @@ def build_app() -> gr.Blocks:
286
  # ------------------------------------------------------------------
287
 
288
  if __name__ == "__main__":
289
- custom_css = """
290
- .form { ¸border: none !important; box-shadow: none !important; gap: 0 !important; }
291
- .form .block, .form .row, .form > * { border: none !important; box-shadow: none !important; }
292
- #excel-btn, #excel-btn.primary {
293
- background: #f97316 !important;
294
- background-color: #f97316 !important;
295
- color: white !important;
296
- border-color: #9a3412 !important;
297
- border: 1px solid #9a3412 !important;
298
- }
299
- #excel-btn:hover, #excel-btn.primary:hover {
300
- background: #ea580c !important;
301
- background-color: #ea580c !important;
302
- border-color: #9a3412 !important;
303
- border: 1px solid #9a3412 !important;
304
- }
305
- #portfolio-footer a {
306
- color: #fa8529 !important;
307
- font-weight: 600 !important;
308
- text-decoration: none !important;
309
- }
310
- #portfolio-footer a:hover {
311
- color: #fb923c !important;
312
- text-decoration: underline !important;
313
- }
314
- """
315
-
316
  port = int(os.environ.get("PORT", 7860))
317
 
318
  application = build_app()
319
- application.launch(server_name="0.0.0.0", server_port=port, share=False, theme=gr.themes.Base(), css=custom_css)
 
22
  method: str,
23
  ):
24
  """Calculate Value at Risk analysis based on Gradio inputs and delegate to the analysis pipeline."""
25
+ logger.debug(
26
  f"Analysis requested: {ticker} | VaR={var_confidence_label} ES={es_confidence_label} | {method} | N={n_days} | Date={end_date_str} | PV=${portfolio_value:,.0f}"
27
  )
28
 
 
69
  stress_label=STRESS_LABEL,
70
  )
71
 
72
+ logger.info(
73
+ f"Analysis complete: {ticker} | VaR=${result['var_nd']:,.2f} | ES=${result['es_nd']:,.2f}"
74
  )
75
 
76
  return (
 
121
  # ------------------------------------------------------------------
122
 
123
 
124
+ CUSTOM_CSS = """
125
+ .form { border: none !important; box-shadow: none !important; gap: 0 !important; }
126
+ .form .block, .form .row, .form > * { border: none !important; box-shadow: none !important; }
127
+ #excel-btn, #excel-btn.primary {
128
+ background: #f97316 !important;
129
+ background-color: #f97316 !important;
130
+ color: white !important;
131
+ border-color: #9a3412 !important;
132
+ border: 1px solid #9a3412 !important;
133
+ }
134
+ #excel-btn:hover, #excel-btn.primary:hover {
135
+ background: #ea580c !important;
136
+ background-color: #ea580c !important;
137
+ border-color: #9a3412 !important;
138
+ border: 1px solid #9a3412 !important;
139
+ }
140
+ #portfolio-hr {
141
+ margin: 2rem 0 0.5rem !important;
142
+ border: none !important;
143
+ border-top: 1px solid #e5e7eb !important;
144
+ }
145
+ #portfolio-footer {
146
+ width: 100% !important;
147
+ max-width: 100% !important;
148
+ }
149
+ #portfolio-text {
150
+ text-align: center !important;
151
+ margin: 0 auto !important;
152
+ padding: 0.75rem 0 !important;
153
+ width: 100% !important;
154
+ }
155
+ #portfolio-text a {
156
+ color: #ffffff !important;
157
+ font-weight: 500 !important;
158
+ font-size: 0.8rem !important;
159
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif !important;
160
+ letter-spacing: 0.05em !important;
161
+ text-decoration: none !important;
162
+ cursor: pointer !important;
163
+ }
164
+ #portfolio-text a:hover {
165
+ color: #d1d5db !important;
166
+ }
167
+ """
168
+
169
+
170
  def build_app() -> gr.Blocks:
171
  """Construct and return the Gradio Blocks application."""
172
 
 
313
  fn=enable_run_button_for_method, inputs=method_radio, outputs=run_btn
314
  )
315
 
316
+ gr.HTML(
317
+ '<hr id="portfolio-hr">'
318
+ '<p id="portfolio-text">'
319
+ '<a href="https://kameshcodes.github.io/portfolio/" target="_blank">'
320
+ "\u00a9 kameshcodes"
 
 
321
  "</a></p>",
322
  elem_id="portfolio-footer",
323
  )
 
330
  # ------------------------------------------------------------------
331
 
332
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  port = int(os.environ.get("PORT", 7860))
334
 
335
  application = build_app()
336
+ application.launch(server_name="0.0.0.0", server_port=port, share=False, theme=gr.themes.Base(), css=CUSTOM_CSS)
config.yaml CHANGED
@@ -2,6 +2,10 @@
2
  # VaR Engine – Application Settings
3
  # ------------------------------------------------------------------
4
 
 
 
 
 
5
  tickers:
6
  - AAPL
7
  - MSFT
@@ -11,9 +15,12 @@ tickers:
11
  - JPM
12
  - BCS
13
 
 
 
14
  lookback_days: 251
15
 
16
- # Stress window used for Stressed VaR/ES
 
17
  stressed_period_label: "Global Financial Crisis (2008)"
18
  stressed_period_start_date: "2008-01-01"
19
  stressed_period_end_date: "2008-12-31"
 
2
  # VaR Engine – Application Settings
3
  # ------------------------------------------------------------------
4
 
5
+ # Environment: dev | test | prod
6
+ env: prod
7
+
8
+ # List of stock tickers available in the UI dropdown.
9
  tickers:
10
  - AAPL
11
  - MSFT
 
15
  - JPM
16
  - BCS
17
 
18
+ # Number of historical trading days used to estimate VaR/ES.
19
+ # Note: n trading days yields n-1 daily returns for VaR estimation.
20
  lookback_days: 251
21
 
22
+ # Stress window used for Stressed VaR/ES.
23
+ # Defines the historical period for stress-testing the portfolio.
24
  stressed_period_label: "Global Financial Crisis (2008)"
25
  stressed_period_start_date: "2008-01-01"
26
  stressed_period_end_date: "2008-12-31"
src/config.py CHANGED
@@ -5,6 +5,7 @@ Usage:
5
  from src.config import TICKERS, LOOKBACK_DAYS, STRESS_LABEL, STRESS_START_DATE, STRESS_END_DATE
6
  """
7
 
 
8
  from pathlib import Path
9
 
10
  import yaml
@@ -14,6 +15,7 @@ _CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.yaml"
14
  with open(_CONFIG_PATH) as _f:
15
  _cfg = yaml.safe_load(_f)
16
 
 
17
  TICKERS: list[str] = _cfg["tickers"]
18
  LOOKBACK_DAYS: int = _cfg["lookback_days"]
19
  STRESS_LABEL: str = _cfg["stressed_period_label"]
 
5
  from src.config import TICKERS, LOOKBACK_DAYS, STRESS_LABEL, STRESS_START_DATE, STRESS_END_DATE
6
  """
7
 
8
+ import os
9
  from pathlib import Path
10
 
11
  import yaml
 
15
  with open(_CONFIG_PATH) as _f:
16
  _cfg = yaml.safe_load(_f)
17
 
18
+ ENV: str = os.environ.get("ENV", _cfg.get("env", "dev")).lower()
19
  TICKERS: list[str] = _cfg["tickers"]
20
  LOOKBACK_DAYS: int = _cfg["lookback_days"]
21
  STRESS_LABEL: str = _cfg["stressed_period_label"]
src/excel_export.py CHANGED
@@ -414,7 +414,7 @@ def _export_sheet(
414
 
415
  workbook.save(path)
416
  action = "sheet added" if stressed else "report saved"
417
- logger.info(f"{method} VaR ES {action}: {path}")
418
  return path
419
 
420
 
 
414
 
415
  workbook.save(path)
416
  action = "sheet added" if stressed else "report saved"
417
+ logger.debug(f"{method} VaR ES {action}: {path}")
418
  return path
419
 
420
 
src/historical.py CHANGED
@@ -74,7 +74,7 @@ def compute_stressed_historical_var_es(
74
 
75
  result = compute_historical_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
76
 
77
- logger.info(
78
  f"Stressed VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
79
  f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
80
  )
@@ -152,7 +152,7 @@ def historical_var_es_pipeline(
152
  ticker=ticker,
153
  )
154
 
155
- logger.info(
156
  f"VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
157
  f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
158
  )
 
74
 
75
  result = compute_historical_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
76
 
77
+ logger.debug(
78
  f"Stressed VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
79
  f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
80
  )
 
152
  ticker=ticker,
153
  )
154
 
155
+ logger.debug(
156
  f"VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
157
  f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
158
  )
src/logger.py CHANGED
@@ -3,6 +3,11 @@ logger.py -- Centralized loguru configuration.
3
 
4
  Import this module once (in app.py) to activate console + file sinks.
5
  All other modules just do `from loguru import logger` directly.
 
 
 
 
 
6
  """
7
 
8
  import sys
@@ -10,6 +15,14 @@ from pathlib import Path
10
 
11
  from loguru import logger
12
 
 
 
 
 
 
 
 
 
13
  # Project root = parent of src/
14
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
15
  LOG_DIR = PROJECT_ROOT / "log"
@@ -18,20 +31,22 @@ LOG_FILE = LOG_DIR / "var_engine.log"
18
  # Remove default stderr handler
19
  logger.remove()
20
 
21
- # Colored console output (INFO+)
22
  logger.add(
23
  sys.stderr,
24
- level="INFO",
25
  colorize=True,
26
  format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>",
27
  )
28
 
29
- # Single rotating file (DEBUG+)
30
  LOG_DIR.mkdir(parents=True, exist_ok=True)
31
  logger.add(
32
  str(LOG_FILE),
33
- level="INFO",
34
  rotation="10 MB",
35
  retention="30 days",
36
  format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
37
  )
 
 
 
3
 
4
  Import this module once (in app.py) to activate console + file sinks.
5
  All other modules just do `from loguru import logger` directly.
6
+
7
+ Log levels by environment (set via ENV variable):
8
+ prod → WARNING (console) / INFO (file)
9
+ test → DEBUG (console) / DEBUG (file)
10
+ dev → INFO (console) / DEBUG (file) [default]
11
  """
12
 
13
  import sys
 
15
 
16
  from loguru import logger
17
 
18
+ from src.config import ENV
19
+
20
+ CONSOLE_LEVELS = {"prod": "WARNING", "test": "DEBUG", "dev": "INFO"}
21
+ FILE_LEVELS = {"prod": "INFO", "test": "DEBUG", "dev": "DEBUG"}
22
+
23
+ console_level = CONSOLE_LEVELS.get(ENV, "INFO")
24
+ file_level = FILE_LEVELS.get(ENV, "DEBUG")
25
+
26
  # Project root = parent of src/
27
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
28
  LOG_DIR = PROJECT_ROOT / "log"
 
31
  # Remove default stderr handler
32
  logger.remove()
33
 
34
+ # Colored console output
35
  logger.add(
36
  sys.stderr,
37
+ level=console_level,
38
  colorize=True,
39
  format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>",
40
  )
41
 
42
+ # Single rotating file
43
  LOG_DIR.mkdir(parents=True, exist_ok=True)
44
  logger.add(
45
  str(LOG_FILE),
46
+ level=file_level,
47
  rotation="10 MB",
48
  retention="30 days",
49
  format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
50
  )
51
+
52
+ logger.info(f"Logger initialized | ENV={ENV} | console={console_level} | file={file_level}")
src/parametric.py CHANGED
@@ -86,7 +86,7 @@ def compute_stressed_parametric_var_es(
86
  daily_returns = compute_returns(prices, kind="log")
87
  result = compute_parametric_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
88
 
89
- logger.info(
90
  f"Stressed Parametric VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
91
  f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
92
  )
@@ -165,7 +165,7 @@ def parametric_var_es_pipeline(
165
  ticker=ticker,
166
  )
167
 
168
- logger.info(
169
  f"Parametric VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
170
  f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
171
  )
 
86
  daily_returns = compute_returns(prices, kind="log")
87
  result = compute_parametric_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
88
 
89
+ logger.debug(
90
  f"Stressed Parametric VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
91
  f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
92
  )
 
165
  ticker=ticker,
166
  )
167
 
168
+ logger.debug(
169
  f"Parametric VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
170
  f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
171
  )
src/utils.py CHANGED
@@ -64,7 +64,7 @@ def fetch_prices(
64
  prices = prices.iloc[start_idx:]
65
  prices = prices.loc[:end_date]
66
 
67
- logger.info(
68
  f"Fetched {len(prices)} trading days for {ticker} "
69
  f"({prices.index[0].strftime('%Y-%m-%d')} to {prices.index[-1].strftime('%Y-%m-%d')})"
70
  )
@@ -100,7 +100,7 @@ def fetch_prices(
100
  prices = pd.Series(df["Close"].squeeze())
101
  prices.name = ticker
102
  result = prices.tail(lookback)
103
- logger.info(
104
  f"Fetched {len(result)} trading days for {ticker} (last date: {result.index[-1].strftime('%Y-%m-%d')})"
105
  )
106
  return result
@@ -203,7 +203,7 @@ def plot_distribution(
203
  template="plotly_white",
204
  yaxis=dict(showgrid=False),
205
  margin=dict(t=80, b=40),
206
- height=388.5,
207
  showlegend=False,
208
  )
209
 
 
64
  prices = prices.iloc[start_idx:]
65
  prices = prices.loc[:end_date]
66
 
67
+ logger.debug(
68
  f"Fetched {len(prices)} trading days for {ticker} "
69
  f"({prices.index[0].strftime('%Y-%m-%d')} to {prices.index[-1].strftime('%Y-%m-%d')})"
70
  )
 
100
  prices = pd.Series(df["Close"].squeeze())
101
  prices.name = ticker
102
  result = prices.tail(lookback)
103
+ logger.debug(
104
  f"Fetched {len(result)} trading days for {ticker} (last date: {result.index[-1].strftime('%Y-%m-%d')})"
105
  )
106
  return result
 
203
  template="plotly_white",
204
  yaxis=dict(showgrid=False),
205
  margin=dict(t=80, b=40),
206
+ height=391,
207
  showlegend=False,
208
  )
209