diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6540506 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +__pycache__/ +*/__pycache__/* +.venv/ +venv/ +.git/ +.github/ +.streamlit/cache/ +data/* diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 94536bc..eab203e 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -4,4 +4,3 @@ primaryColor = "#F97316" backgroundColor = "#0F1115" secondaryBackgroundColor = "#1A1D23" textColor = "#E5E7EB" -font = "sans serif" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0c9b120 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app +ENV PIP_NO_CACHE_DIR=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8501 + +CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"] diff --git a/README.md b/README.md index 0e7fa18..d3cc596 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,12 @@ source .venv/bin/activate python -m pip install --upgrade pip pip install -r requirements.txt python -m streamlit run streamlit_app.py + +### Ejecutar con Docker +```bash +# Construir la imagen +docker build -t quantboard . + +# Ejecutar el contenedor (Streamlit en http://localhost:8501) +docker run -p 8501:8501 quantboard +``` diff --git a/pages/02_SMA_Heatmap.py b/pages/02_SMA_Heatmap.py index 58e6e30..f77c915 100644 --- a/pages/02_SMA_Heatmap.py +++ b/pages/02_SMA_Heatmap.py @@ -65,21 +65,27 @@ def main() -> None: ticker = st.text_input("Ticker", value=ticker_default).strip().upper() end = st.date_input("To", value=end_default) start = st.date_input("From", value=start_default) - fast_min, fast_max = st.slider("Fast SMA range", 5, 60, (fast_min_default, fast_max_default)) - slow_min, slow_max = st.slider("Slow SMA range", 20, 240, (slow_min_default, slow_max_default)) + fast_min, fast_max = st.slider( + "Fast SMA range", 5, 60, (fast_min_default, fast_max_default) + ) + slow_min, slow_max = st.slider( + "Slow SMA range", 20, 240, (slow_min_default, slow_max_default) + ) submitted = st.form_submit_button("Run search", type="primary") - if not submitted: - st.info("Choose parameters and click **Run search**.") - return + # If the form was not submitted, show a hint and exit early + if not submitted: + st.info("Choose parameters and click **Run search**.") + return - set_param("ticker", ticker or None) - set_param("heat_end", end) - set_param("heat_start", start) - set_param("heat_fast_min", int(fast_min)) - set_param("heat_fast_max", int(fast_max)) - set_param("heat_slow_min", int(slow_min)) - set_param("heat_slow_max", int(slow_max)) + # Persist parameters for sharable links / reload + set_param("ticker", ticker or None) + set_param("heat_end", end) + set_param("heat_start", start) + set_param("heat_fast_min", int(fast_min)) + set_param("heat_fast_max", int(fast_max)) + set_param("heat_slow_min", int(slow_min)) + set_param("heat_slow_max", int(slow_max)) if fast_min >= slow_min: st.error("Fast SMA range must stay below the Slow SMA range.") @@ -107,7 +113,10 @@ def main() -> None: z.loc[fast_window, slow_window] = float("nan") st.subheader("Heatmap (Sharpe)") - st.plotly_chart(heatmap_metric(z, title="SMA grid — Sharpe"), use_container_width=True) + st.plotly_chart( + heatmap_metric(z, title="SMA grid — Sharpe"), + use_container_width=True, + ) stacked = z.stack().dropna().astype(float) if stacked.empty: diff --git a/pages/03_Backtest.py b/pages/03_Backtest.py index f82b0d6..6c9f7d0 100644 --- a/pages/03_Backtest.py +++ b/pages/03_Backtest.py @@ -105,6 +105,31 @@ def _run_signals(close: pd.Series, fast: int, slow: int) -> pd.Series: return sig.replace(0, pd.NA).ffill().fillna(0.0) +def _clean_prices(df: pd.DataFrame) -> pd.DataFrame | None: + price_cols = ["open", "high", "low", "close", "volume"] + numeric = {col: pd.to_numeric(df.get(col), errors="coerce") for col in price_cols if col in df} + clean = df.assign(**numeric).dropna(subset=["close"]) + if clean.empty: + st.error("No price data available after cleaning.") + return None + return clean + + +def _run_signals(close: pd.Series, fast: int, slow: int) -> pd.Series: + if signals_sma_crossover is not None: + sig, _ = signals_sma_crossover(close, fast=fast, slow=slow) + return sig + + sma_fast = sma(close, fast) + sma_slow = sma(close, slow) + cross_up = (sma_fast > sma_slow) & (sma_fast.shift(1) <= sma_slow.shift(1)) + cross_dn = (sma_fast < sma_slow) & (sma_fast.shift(1) >= sma_slow.shift(1)) + sig = pd.Series(0.0, index=close.index) + sig = sig.where(~cross_up, 1.0) + sig = sig.where(~cross_dn, -1.0) + return sig.replace(0, pd.NA).ffill().fillna(0.0) + + def main() -> None: st.title("Backtest — SMA crossover") @@ -142,12 +167,7 @@ def main() -> None: fee_bps = st.number_input("Fees (bps)", 0, 50, int(fee_default), step=1) slip_bps = st.number_input("Slippage (bps)", 0, 50, int(slip_default), step=1) submitted = st.form_submit_button("Run backtest", type="primary") - - st.info("Configure the sidebar parameters and run the backtest.") - - if not submitted: - return - + set_param("ticker", ticker or None) set_param("bt_end", end) set_param("bt_start", start) @@ -157,6 +177,18 @@ def main() -> None: set_param("fee_bps", int(fee_bps)) set_param("slippage_bps", int(slip_bps)) + st.info("Configure the sidebar parameters and run the backtest.") + + if not submitted: + return + + if fast >= slow: + st.error("Fast SMA must be smaller than Slow SMA.") + return + + with st.spinner("Fetching data..."): + df = _load_prices(ticker, start=start, end=end) + if fast >= slow: st.error("Fast SMA must be smaller than Slow SMA.") return diff --git a/pages/2_Optimizacion_SMA.py b/pages/2_Optimizacion_SMA.py index bd892ca..d35780b 100644 --- a/pages/2_Optimizacion_SMA.py +++ b/pages/2_Optimizacion_SMA.py @@ -7,5 +7,7 @@ shareable_link_button() -st.info("Esta página fue reemplazada por **SMA Heatmap**. Usá el ítem *SMA Heatmap* del menú para optimizar parámetros.") - +st.info( + "This page was replaced by **SMA Heatmap**. " + "Use the *SMA Heatmap* menu item to optimize parameters." +)