Spaces:
Running
Running
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- app.py +55 -38
- config.yaml +8 -1
- src/config.py +2 -0
- src/excel_export.py +1 -1
- src/historical.py +2 -2
- src/logger.py +19 -4
- src/parametric.py +2 -2
- 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.
|
| 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.
|
| 73 |
-
f"Analysis complete: VaR=${result['var_nd']:,.2f}
|
| 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.
|
| 271 |
-
'<hr
|
| 272 |
-
'<p
|
| 273 |
-
'<a href="https://kameshcodes.github.io/portfolio/" target="_blank"
|
| 274 |
-
|
| 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=
|
|
|
|
| 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.
|
| 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.
|
| 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.
|
| 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
|
| 22 |
logger.add(
|
| 23 |
sys.stderr,
|
| 24 |
-
level=
|
| 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
|
| 30 |
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
| 31 |
logger.add(
|
| 32 |
str(LOG_FILE),
|
| 33 |
-
level=
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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=
|
| 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 |
|