diff --git a/data/loader.py b/data/loader.py index b0f0580004b489f9ac41f20977177f6f192bc44f..7f5795411e327196e36563def95a0792d9a0e5cd 100644 --- a/data/loader.py +++ b/data/loader.py @@ -1,49 +1,100 @@ -def sync_incremental_data(df): - # Ensure index is datetime - df.index = pd.to_datetime(df.index) - last_date = df.index.max() - +import pandas as pd +import pandas_datareader.data as web +import yfinance as yf +from huggingface_hub import hf_hub_download, HfApi +import os +import streamlit as st + +# Explicitly define the lists first so they are available for export +X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] +FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] + +REPO_ID = "P2SAMAPA/etf_trend_data" +FILENAME = "market_data.csv" + +def get_safe_token(): + try: return st.secrets["HF_TOKEN"] + except: return os.getenv("HF_TOKEN") + +def load_from_hf(): + """Initial load function called by app.py""" + token = get_safe_token() + if not token: + return None + try: + path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) + df = pd.read_csv(path, index_col=0, parse_dates=True) + return df.ffill() # Handle internal NaNs immediately + except Exception as e: + st.error(f"HF Load Error: {e}") + return None + +def seed_dataset_from_scratch(): + """Initializes the CSV with full history""" tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + data = yf.download(tickers, start="2008-01-01", progress=False) - # Calculate sync start (day after last record) + # Robustly handle Column Multi-Index + if 'Adj Close' in data.columns: + master_df = data['Adj Close'] + else: + master_df = data['Close'] + + try: + sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() + master_df['SOFR_ANNUAL'] = sofr / 100 + except: + master_df['SOFR_ANNUAL'] = 0.045 + + master_df = master_df.sort_index().ffill() + master_df.to_csv(FILENAME) + upload_to_hf(FILENAME) + return master_df + +def sync_incremental_data(df): + """The function triggered by the Sync Button""" + if df is None: + return seed_dataset_from_scratch() + + last_date = pd.to_datetime(df.index.max()) sync_start = last_date + pd.Timedelta(days=1) - # If sync_start is in the future, nothing to do + # Check if data is already current to today if sync_start > pd.Timestamp.now().normalize(): - st.info("Data is already up to date.") + st.toast("Data is already up to date!", icon="✅") return df + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + try: - # Download new data new_data_raw = yf.download(tickers, start=sync_start, progress=False) if new_data_raw.empty: - st.warning("No new market data found to sync.") + st.toast("No new market sessions found.", icon="â„šī¸") return df - # Handle columns + # Column selection if 'Adj Close' in new_data_raw.columns: new_data = new_data_raw['Adj Close'] else: new_data = new_data_raw['Close'] - # Clean NaNs before merging - new_data = new_data.dropna(how='all') - - # Combine, sort, and deduplicate + # Merge and clean combined = pd.concat([df, new_data]).sort_index() - combined = combined[~combined.index.duplicated(keep='last')] + combined = combined[~combined.index.duplicated(keep='last')].ffill() - # Forward fill any holes in the middle, but don't fill the end - combined = combined.ffill() - - # Save and Push + # Save locally and push to cloud combined.to_csv(FILENAME) upload_to_hf(FILENAME) - st.success(f"Synced successfully up to {combined.index.max().date()}") + st.toast(f"Sync complete: {combined.index.max().date()}", icon="🚀") return combined - except Exception as e: - st.error(f"Sync failed error: {e}") + st.error(f"Sync failed: {e}") return df + +def upload_to_hf(path): + token = get_safe_token() + if token: + api = HfApi() + api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) diff --git a/hf_space/engine/trend_engine.py b/hf_space/engine/trend_engine.py index a0d0a193b9fcf88520a81ad2e5c720c73ababdbe..569e6c9584654bd785a7b0cf7c7b982917f3ce49 100644 --- a/hf_space/engine/trend_engine.py +++ b/hf_space/engine/trend_engine.py @@ -3,71 +3,110 @@ import numpy as np import pandas_market_calendars as mcal def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, sub_option): - # 1. Trend & Conviction Logic + """ + Quantitative Engine based on Zarattini & Antonacci (2025). + Implements Volatility Targeting and Conviction-based ETF Allocation. + """ + + # --- 1. DATA CLEANING & PREPARATION --- + # Forward fill holes and drop assets with no data in the current window + price_df = price_df.ffill() + # Ensure benchmarks and SOFR are aligned + sofr_series = sofr_series.ffill() + + # --- 2. TREND & CONVICTION SIGNALS --- sma_200 = price_df.rolling(200).mean() sma_50 = price_df.rolling(50).mean() - # Conviction = Percentage distance above the 200 SMA + # Conviction = % distance above the 200 SMA (momentum strength) conviction_score = (price_df / sma_200) - 1 - signals = (sma_50 > sma_200).astype(int) + # Basic Signal: 50 SMA > 200 SMA + base_signals = (sma_50 > sma_200).astype(int) + + # --- 3. CONVICTION FILTERING (Sub-Options) --- + if sub_option == "3 Highest Conviction": + # Rank assets daily; 1 is highest conviction. + # Only assets in a base trend (base_signals == 1) are eligible for ranking. + ranked_conviction = conviction_score.where(base_signals == 1) + ranks = ranked_conviction.rank(axis=1, ascending=False) + final_signals = ((ranks <= 3)).astype(int) + elif sub_option == "1 Highest Conviction": + ranked_conviction = conviction_score.where(base_signals == 1) + ranks = ranked_conviction.rank(axis=1, ascending=False) + final_signals = ((ranks <= 1)).astype(int) + else: + # "All Trending ETFs" + final_signals = base_signals - # 2. Risk Metrics + # --- 4. VOLATILITY TARGETING (RISK BUDGETING) --- returns = price_df.pct_change() + # 60-day Annualized Realized Volatility asset_vol = returns.rolling(60).std() * np.sqrt(252) - # 3. Apply Sub-Option Concentration - if sub_option == "3 Highest Conviction": - ranks = conviction_score.rank(axis=1, ascending=False) - signals = ((ranks <= 3) & (signals == 1)).astype(int) - elif sub_option == "1 Highest Conviction": - ranks = conviction_score.rank(axis=1, ascending=False) - signals = ((ranks <= 1) & (signals == 1)).astype(int) + # Safety: If vol is NaN or 0, set to a very high number to prevent infinite weights + asset_vol = asset_vol.replace(0, np.nan).fillna(9.99) - # 4. Volatility Target Weighting - active_counts = signals.sum(axis=1) - # Target Vol / Asset Vol, distributed across active signals + # Methodology: Target Vol / Asset Vol, distributed across active signals + active_counts = final_signals.sum(axis=1) + # Avoid division by zero if no assets are in trend raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).replace([np.inf, -np.inf], 0).fillna(0) - final_weights = raw_weights * signals - # 5. Leverage Cap (1.5x) + # Multiply by signals to zero out non-trending assets + final_weights = raw_weights * final_signals + + # --- 5. EXPOSURE & LEVERAGE MANAGEMENT --- total_exposure = final_weights.sum(axis=1) - scale_factor = total_exposure.apply(lambda x: 1.5/x if x > 1.5 else 1.0) + # Cap total gross leverage at 1.5x (150%) + leverage_cap = 1.5 + scale_factor = total_exposure.apply(lambda x: leverage_cap/x if x > leverage_cap else 1.0) final_weights = final_weights.multiply(scale_factor, axis=0) - # 6. Cash (SOFR) Allocation - cash_weight = 1.0 - final_weights.sum(axis=1) + # --- 6. CASH (SOFR) ALLOCATION --- + # Remainder of the 100% capital not used in the risk budget goes to SOFR + final_exposure = final_weights.sum(axis=1) + cash_weight = 1.0 - final_exposure - # 7. Portfolio Returns + # --- 7. PERFORMANCE CALCULATION --- + # Strategy Return = (Weights * Asset Returns) + (Cash Weight * SOFR) + # We shift weights by 1 to prevent look-ahead bias (trading at today's close for tomorrow) portfolio_ret = (final_weights.shift(1) * returns).sum(axis=1) portfolio_ret += cash_weight.shift(1) * (sofr_series.shift(1) / 252) - # 8. Out-of-Sample Slicing + # --- 8. OUT-OF-SAMPLE (OOS) METRICS --- oos_mask = portfolio_ret.index.year >= start_yr - equity_curve = (1 + portfolio_ret[oos_mask]).cumprod() - bench_curve = (1 + bench_series.pct_change().fillna(0)[oos_mask]).cumprod() + oos_returns = portfolio_ret[oos_mask] + + equity_curve = (1 + oos_returns).cumprod() + bench_returns = bench_series.pct_change().fillna(0)[oos_mask] + bench_curve = (1 + bench_returns).cumprod() + + # Drawdowns + dd_series = (equity_curve / equity_curve.cummax()) - 1 # Stats - ann_ret = portfolio_ret[oos_mask].mean() * 252 - ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) - dd = (equity_curve / equity_curve.cummax()) - 1 + ann_ret = oos_returns.mean() * 252 + ann_vol = oos_returns.std() * np.sqrt(252) + current_sofr = sofr_series.ffill().iloc[-1] + + # Sharpe Ratio: (Return - RiskFree) / Vol + sharpe = (ann_ret - current_sofr) / ann_vol if ann_vol > 0 else 0 - # --- NEXT DAY TRADING LOGIC --- + # --- 9. NEXT TRADING DAY CALENDAR --- nyse = mcal.get_calendar('NYSE') - # Use real-world today to anchor the search for the NEXT session + # Anchor to system clock to ensure we always look FORWARD today_dt = pd.Timestamp.now().normalize() search_start = today_dt + pd.Timedelta(days=1) sched = nyse.schedule(start_date=search_start, end_date=search_start + pd.Timedelta(days=10)) - next_day = sched.index[0] + next_day = sched.index[0] return { 'equity_curve': equity_curve, 'bench_curve': bench_curve, 'ann_ret': ann_ret, - 'sharpe': (ann_ret - sofr_series.iloc[-1]) / ann_vol if ann_vol > 0 else 0, - 'max_dd': dd.min(), - 'avg_daily_dd': dd.mean(), + 'sharpe': sharpe, + 'max_dd': dd_series.min(), 'next_day': next_day.date(), 'current_weights': final_weights.iloc[-1], 'cash_weight': cash_weight.iloc[-1], - 'current_sofr': sofr_series.iloc[-1] + 'current_sofr': current_sofr } diff --git a/hf_space/hf_space/data/loader.py b/hf_space/hf_space/data/loader.py index 1a7c1eee50b61e7ab279bcbd5f09d1363b3218a2..b0f0580004b489f9ac41f20977177f6f192bc44f 100644 --- a/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/data/loader.py @@ -1,90 +1,49 @@ -import pandas as pd -import pandas_datareader.data as web -import yfinance as yf -from huggingface_hub import hf_hub_download, HfApi -import os -import streamlit as st - -# 1. Define the Ticker Lists -X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] -FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] - -REPO_ID = "P2SAMAPA/etf_trend_data" -FILENAME = "market_data.csv" - -def get_safe_token(): - try: return st.secrets["HF_TOKEN"] - except: return os.getenv("HF_TOKEN") - -# 2. Define load_from_hf -def load_from_hf(): - token = get_safe_token() - if not token: - return None - try: - path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) - return pd.read_csv(path, index_col=0, parse_dates=True) - except Exception as e: - st.warning(f"Could not load from HuggingFace: {e}") - return None - -# 3. Define seed_dataset_from_scratch -def seed_dataset_from_scratch(): - tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - data = yf.download(tickers, start="2008-01-01", progress=False) - - # Handle the 'Adj Close' multi-index issue - if 'Adj Close' in data.columns: - master_df = data['Adj Close'] - else: - master_df = data['Close'] - - # Add SOFR from FRED - try: - sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() - master_df['SOFR_ANNUAL'] = sofr / 100 - except: - master_df['SOFR_ANNUAL'] = 0.045 # Default fallback - - master_df = master_df.sort_index().ffill() - master_df.to_csv(FILENAME) - upload_to_hf(FILENAME) - return master_df - -# 4. Define sync_incremental_data def sync_incremental_data(df): - last_date = pd.to_datetime(df.index.max()) + # Ensure index is datetime + df.index = pd.to_datetime(df.index) + last_date = df.index.max() + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + + # Calculate sync start (day after last record) sync_start = last_date + pd.Timedelta(days=1) - if sync_start > pd.Timestamp.now(): + # If sync_start is in the future, nothing to do + if sync_start > pd.Timestamp.now().normalize(): + st.info("Data is already up to date.") return df try: + # Download new data new_data_raw = yf.download(tickers, start=sync_start, progress=False) + if new_data_raw.empty: + st.warning("No new market data found to sync.") return df + # Handle columns if 'Adj Close' in new_data_raw.columns: new_data = new_data_raw['Adj Close'] else: new_data = new_data_raw['Close'] + # Clean NaNs before merging + new_data = new_data.dropna(how='all') + + # Combine, sort, and deduplicate combined = pd.concat([df, new_data]).sort_index() combined = combined[~combined.index.duplicated(keep='last')] + # Forward fill any holes in the middle, but don't fill the end + combined = combined.ffill() + + # Save and Push combined.to_csv(FILENAME) upload_to_hf(FILENAME) + + st.success(f"Synced successfully up to {combined.index.max().date()}") return combined + except Exception as e: - st.error(f"Sync failed: {e}") + st.error(f"Sync failed error: {e}") return df - -def upload_to_hf(path): - token = get_safe_token() - if token: - api = HfApi() - try: - api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) - except Exception as e: - st.error(f"HF Upload failed: {e}") diff --git a/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/data/loader.py index ad6222dfb7ec282b9a27a39c863a9234502141ce..1a7c1eee50b61e7ab279bcbd5f09d1363b3218a2 100644 --- a/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/data/loader.py @@ -1,40 +1,90 @@ +import pandas as pd +import pandas_datareader.data as web +import yfinance as yf +from huggingface_hub import hf_hub_download, HfApi +import os +import streamlit as st + +# 1. Define the Ticker Lists +X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] +FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] + +REPO_ID = "P2SAMAPA/etf_trend_data" +FILENAME = "market_data.csv" + +def get_safe_token(): + try: return st.secrets["HF_TOKEN"] + except: return os.getenv("HF_TOKEN") + +# 2. Define load_from_hf +def load_from_hf(): + token = get_safe_token() + if not token: + return None + try: + path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) + return pd.read_csv(path, index_col=0, parse_dates=True) + except Exception as e: + st.warning(f"Could not load from HuggingFace: {e}") + return None + +# 3. Define seed_dataset_from_scratch +def seed_dataset_from_scratch(): + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + data = yf.download(tickers, start="2008-01-01", progress=False) + + # Handle the 'Adj Close' multi-index issue + if 'Adj Close' in data.columns: + master_df = data['Adj Close'] + else: + master_df = data['Close'] + + # Add SOFR from FRED + try: + sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() + master_df['SOFR_ANNUAL'] = sofr / 100 + except: + master_df['SOFR_ANNUAL'] = 0.045 # Default fallback + + master_df = master_df.sort_index().ffill() + master_df.to_csv(FILENAME) + upload_to_hf(FILENAME) + return master_df + +# 4. Define sync_incremental_data def sync_incremental_data(df): last_date = pd.to_datetime(df.index.max()) tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) sync_start = last_date + pd.Timedelta(days=1) - # Check if we even need to sync (avoiding weekend/pre-market errors) if sync_start > pd.Timestamp.now(): return df try: - # Download from yfinance with specific configuration to avoid Multi-Index issues - new_data_raw = yf.download(tickers, start=sync_start, progress=False, group_by='column') - + new_data_raw = yf.download(tickers, start=sync_start, progress=False) if new_data_raw.empty: return df - # Logic to handle different yfinance return structures if 'Adj Close' in new_data_raw.columns: new_data = new_data_raw['Adj Close'] - elif 'Close' in new_data_raw.columns: - new_data = new_data_raw['Close'] else: - # If it's a single ticker or flattened - new_data = new_data_raw + new_data = new_data_raw['Close'] - # Standardize: Ensure we only have the tickers we want and no empty columns - new_data = new_data[new_data.columns.intersection(tickers)] - - # Combine with master dataframe combined = pd.concat([df, new_data]).sort_index() - # Keep the most recent data point if duplicates occur combined = combined[~combined.index.duplicated(keep='last')] combined.to_csv(FILENAME) upload_to_hf(FILENAME) return combined - except Exception as e: st.error(f"Sync failed: {e}") return df + +def upload_to_hf(path): + token = get_safe_token() + if token: + api = HfApi() + try: + api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) + except Exception as e: + st.error(f"HF Upload failed: {e}") diff --git a/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/data/loader.py index 02059f1978476193b6dbfec9e6c8492e5a322828..ad6222dfb7ec282b9a27a39c863a9234502141ce 100644 --- a/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,81 +1,40 @@ -import pandas as pd -import pandas_datareader.data as web -import yfinance as yf -import time -from huggingface_hub import hf_hub_download, HfApi -import os -import streamlit as st - -REPO_ID = "P2SAMAPA/etf_trend_data" -FILENAME = "market_data.csv" - -X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] -FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] - -def get_safe_token(): - """Bypasses Streamlit's hard-fail on missing secrets.toml by using environment fallback.""" - try: - # Try Streamlit secrets first - return st.secrets["HF_TOKEN"] - except Exception: - # Standard environment variable fallback (How HF actually stores them) - return os.getenv("HF_TOKEN") - -def load_from_hf(): - token = get_safe_token() - if not token: return None - try: - path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) - return pd.read_csv(path, index_col=0, parse_dates=True) - except: - return None - -def seed_dataset_from_scratch(): - # Include benchmarks for comparison logic +def sync_incremental_data(df): + last_date = pd.to_datetime(df.index.max()) tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - master_df = pd.DataFrame() - status = st.empty() - progress = st.progress(0) + sync_start = last_date + pd.Timedelta(days=1) - for i, t in enumerate(tickers): - status.text(f"đŸ›°ī¸ Fetching {t} from Stooq...") - try: - data = web.DataReader(f"{t}.US", 'stooq', start='2008-01-01') - if not data.empty: - master_df[t] = data['Close'].sort_index() - time.sleep(0.7) # Polite delay - except: - try: - master_df[t] = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] - except: pass - progress.progress((i + 1) / len(tickers)) + # Check if we even need to sync (avoiding weekend/pre-market errors) + if sync_start > pd.Timestamp.now(): + return df - # Add SOFR Rate (Cash Interest) try: - sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() - master_df['SOFR_ANNUAL'] = sofr / 100 - except: - master_df['SOFR_ANNUAL'] = 0.045 - - master_df = master_df.sort_index().ffill() - master_df.to_csv(FILENAME) - upload_to_hf(FILENAME) - return master_df - -def sync_incremental_data(df): - last_date = df.index.max() - tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] - combined = pd.concat([df, new_data]).sort_index() - combined = combined[~combined.index.duplicated(keep='last')] - combined.to_csv(FILENAME) - upload_to_hf(FILENAME) - return combined - -def upload_to_hf(path): - token = get_safe_token() - if not token: - st.error("❌ Cannot upload: HF_TOKEN is missing from Space Secrets.") - return - api = HfApi() - api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) + # Download from yfinance with specific configuration to avoid Multi-Index issues + new_data_raw = yf.download(tickers, start=sync_start, progress=False, group_by='column') + + if new_data_raw.empty: + return df + + # Logic to handle different yfinance return structures + if 'Adj Close' in new_data_raw.columns: + new_data = new_data_raw['Adj Close'] + elif 'Close' in new_data_raw.columns: + new_data = new_data_raw['Close'] + else: + # If it's a single ticker or flattened + new_data = new_data_raw + + # Standardize: Ensure we only have the tickers we want and no empty columns + new_data = new_data[new_data.columns.intersection(tickers)] + + # Combine with master dataframe + combined = pd.concat([df, new_data]).sort_index() + # Keep the most recent data point if duplicates occur + combined = combined[~combined.index.duplicated(keep='last')] + + combined.to_csv(FILENAME) + upload_to_hf(FILENAME) + return combined + + except Exception as e: + st.error(f"Sync failed: {e}") + return df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 610eeb78af93610d6ce5b478f1b3fb1f20e131b0..5fc44b3841a08115e73d8ef738ef41f35fcd0576 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -6,7 +6,6 @@ from engine.trend_engine import run_trend_module st.set_page_config(layout="wide", page_title="P2 Strategy Suite") -# Initialize Session State safely if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() @@ -43,14 +42,14 @@ if st.session_state.master_data is not None: st.title(f"📊 {option}: {sub_option}") - # Row 1: Metrics + # Row 1: Metrics (Annual Return First) m1, m2, m3, m4 = st.columns(4) m1.metric("Annual Return", f"{results['ann_ret']:.1%}") m2.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") m3.metric("Max Drawdown", f"{results['max_dd']:.1%}") m4.metric("Current SOFR", f"{results['current_sofr']:.2%}") - # Row 2: Performance Chart + # Row 2: Performance Chart (Interactive Years) fig = go.Figure() fig.add_trace(go.Scatter(x=results['equity_curve'].index, y=results['equity_curve'], name='Strategy')) fig.add_trace(go.Scatter(x=results['bench_curve'].index, y=results['bench_curve'], name=f'Benchmark ({bench})')) @@ -74,10 +73,10 @@ if st.session_state.master_data is not None: This strategy implements the **2025 Charles H. Dow Award** winning framework by **Andrea Zarattini** and **Michael Antonacci**. 1. **Regime Identification**: A dual 50/200-day SMA filter determines asset eligibility. - 2. **Conviction Ranking**: Assets are ranked by their distance from the 200-day SMA. - 3. **Concentrated Sizing**: Under the **{sub_option}** setting, the system focuses the risk budget only on the top leaders. + 2. **Conviction Ranking**: Assets are ranked by their distance from the 200-day SMA (Trend Strength). + 3. **Concentrated Sizing**: In **{sub_option}** mode, the risk budget is focused only on top leaders. 4. **Volatility Targeting**: Allocations are sized inversely to 60-day volatility to maintain a stable **{vol_target:.0%}** risk profile. - 5. **Cash Buffer**: Remaining budget earns the live SOFR rate. + 5. **Cash Buffer**: Remaining budget earns the live SOFR rate (Federal Reserve Bank of New York). """) else: st.info("💡 Adjust settings and click 'Run Analysis'.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index d16607f5a5b49e28edd41696f75abfc6cad24212..a0d0a193b9fcf88520a81ad2e5c720c73ababdbe 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -25,6 +25,7 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, # 4. Volatility Target Weighting active_counts = signals.sum(axis=1) + # Target Vol / Asset Vol, distributed across active signals raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).replace([np.inf, -np.inf], 0).fillna(0) final_weights = raw_weights * signals @@ -50,17 +51,13 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) dd = (equity_curve / equity_curve.cummax()) - 1 - # --- FIXED NEXT DAY LOGIC --- + # --- NEXT DAY TRADING LOGIC --- nyse = mcal.get_calendar('NYSE') - last_dt = price_df.index[-1] - - # Generate schedule starting from the day AFTER last_dt to ensure we find the future open - search_start = last_dt + pd.Timedelta(days=1) + # Use real-world today to anchor the search for the NEXT session + today_dt = pd.Timestamp.now().normalize() + search_start = today_dt + pd.Timedelta(days=1) sched = nyse.schedule(start_date=search_start, end_date=search_start + pd.Timedelta(days=10)) - - # Take the first valid trading day from the future schedule next_day = sched.index[0] - # ---------------------------- return { 'equity_curve': equity_curve, @@ -68,6 +65,7 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, 'ann_ret': ann_ret, 'sharpe': (ann_ret - sofr_series.iloc[-1]) / ann_vol if ann_vol > 0 else 0, 'max_dd': dd.min(), + 'avg_daily_dd': dd.mean(), 'next_day': next_day.date(), 'current_weights': final_weights.iloc[-1], 'cash_weight': cash_weight.iloc[-1], diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index a3bfe754990b0f15ef813b872879b22bc4bf5275..d16607f5a5b49e28edd41696f75abfc6cad24212 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -17,16 +17,13 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, # 3. Apply Sub-Option Concentration if sub_option == "3 Highest Conviction": - # Rank daily: 1 is highest conviction ranks = conviction_score.rank(axis=1, ascending=False) signals = ((ranks <= 3) & (signals == 1)).astype(int) elif sub_option == "1 Highest Conviction": ranks = conviction_score.rank(axis=1, ascending=False) signals = ((ranks <= 1) & (signals == 1)).astype(int) - # Else: "All Trending ETFs" uses the base signals # 4. Volatility Target Weighting - # Methodology: Target Vol / Asset Vol, distributed across active signals active_counts = signals.sum(axis=1) raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).replace([np.inf, -np.inf], 0).fillna(0) final_weights = raw_weights * signals @@ -53,10 +50,17 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) dd = (equity_curve / equity_curve.cummax()) - 1 - # NYSE Calendar + # --- FIXED NEXT DAY LOGIC --- nyse = mcal.get_calendar('NYSE') last_dt = price_df.index[-1] - next_day = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)).index[1] + + # Generate schedule starting from the day AFTER last_dt to ensure we find the future open + search_start = last_dt + pd.Timedelta(days=1) + sched = nyse.schedule(start_date=search_start, end_date=search_start + pd.Timedelta(days=10)) + + # Take the first valid trading day from the future schedule + next_day = sched.index[0] + # ---------------------------- return { 'equity_curve': equity_curve, diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 0e5b7a6519a9e812cd0d2b79b40f1d6975a4eb84..610eeb78af93610d6ce5b478f1b3fb1f20e131b0 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -6,19 +6,28 @@ from engine.trend_engine import run_trend_module st.set_page_config(layout="wide", page_title="P2 Strategy Suite") +# Initialize Session State safely if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() with st.sidebar: st.header("đŸ—‚ī¸ Configuration") + if st.session_state.master_data is None: + if st.button("🚀 Seed Database"): + st.session_state.master_data = seed_dataset_from_scratch() + st.rerun() + else: + st.success(f"Sync: {st.session_state.master_data.index.max().date()}") + if st.button("🔄 Sync New Data"): + st.session_state.master_data = sync_incremental_data(st.session_state.master_data) + st.rerun() + + st.divider() option = st.selectbox("Universe Selection", ("Option A - FI Trend", "Option B - Equity Trend")) - - # NEW SUB-OPTIONS - sub_option = st.selectbox("Conviction Level", + sub_option = st.selectbox("Conviction Strategy", ("All Trending ETFs", "3 Highest Conviction", "1 Highest Conviction")) - - start_yr = st.slider("OOS Start", 2008, 2026, 2018) - vol_target = st.slider("Volatility Target (%)", 5, 20, 12) / 100 + start_yr = st.slider("OOS Start Year", 2008, 2026, 2018) + vol_target = st.slider("Risk Target (%)", 5, 20, 12) / 100 run_btn = st.button("🚀 Run Analysis", use_container_width=True, type="primary") if st.session_state.master_data is not None: @@ -32,48 +41,43 @@ if st.session_state.master_data is not None: st.session_state.master_data['SOFR_ANNUAL'], vol_target, start_yr, sub_option) - st.title(f"📊 {option} - {sub_option}") + st.title(f"📊 {option}: {sub_option}") - # Metrics + # Row 1: Metrics m1, m2, m3, m4 = st.columns(4) m1.metric("Annual Return", f"{results['ann_ret']:.1%}") m2.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") m3.metric("Max Drawdown", f"{results['max_dd']:.1%}") m4.metric("Current SOFR", f"{results['current_sofr']:.2%}") - # Chart + # Row 2: Performance Chart fig = go.Figure() fig.add_trace(go.Scatter(x=results['equity_curve'].index, y=results['equity_curve'], name='Strategy')) fig.add_trace(go.Scatter(x=results['bench_curve'].index, y=results['bench_curve'], name=f'Benchmark ({bench})')) - fig.update_layout(title="OOS Performance", template="plotly_dark") + fig.update_layout(title="Out-of-Sample Performance", template="plotly_dark", xaxis_title="Year") st.plotly_chart(fig, use_container_width=True) - # Methodology & Target + # Row 3: Methodology & Allocations st.divider() col_left, col_right = st.columns([1, 1.5]) with col_left: - st.subheader(f"đŸŽ¯ Target Allocation: {results['next_day']}") + st.subheader(f"đŸŽ¯ Allocation for {results['next_day']}") w = results['current_weights'][results['current_weights'] > 0.0001].to_dict() w['CASH (SOFR)'] = results['cash_weight'] - st.table(pd.DataFrame.from_dict(w, orient='index', columns=['Weight']).style.format("{:.2%}")) + df_w = pd.DataFrame.from_dict(w, orient='index', columns=['Weight']) + st.table(df_w.style.format("{:.2%}")) with col_right: - st.subheader("📚 Methodology: Zarattini & Antonacci (2025)") + st.subheader("📚 Methodology: Zarattini & Antonacci") st.markdown(f""" - This strategy implements the **2025 Charles H. Dow Award** framework authored by **Andrea Zarattini** and **Michael Antonacci**. + This strategy implements the **2025 Charles H. Dow Award** winning framework by **Andrea Zarattini** and **Michael Antonacci**. - * **Trend Detection**: Uses a 50/200 SMA dual-filter. - * **Conviction Scoring**: Assets are ranked based on their relative distance from the 200-day trend line. - * **Concentration**: Under **{sub_option}**, the engine filters the universe to only the top-tier trending assets. - * **Risk Sizing**: Allocation is inversely proportional to 60-day volatility. If the selected ETFs cannot safely fill the **{vol_target:.0%}** risk budget, the remainder is held in **CASH (SOFR)**. + 1. **Regime Identification**: A dual 50/200-day SMA filter determines asset eligibility. + 2. **Conviction Ranking**: Assets are ranked by their distance from the 200-day SMA. + 3. **Concentrated Sizing**: Under the **{sub_option}** setting, the system focuses the risk budget only on the top leaders. + 4. **Volatility Targeting**: Allocations are sized inversely to 60-day volatility to maintain a stable **{vol_target:.0%}** risk profile. + 5. **Cash Buffer**: Remaining budget earns the live SOFR rate. """) - - - -### Why this is powerful: -* **The "3 Highest Conviction" sub-option** creates a "Best of the Best" portfolio. Instead of diluting your risk budget across 20 ETFs that are barely in trend, it puts the full 12% risk budget into the 3 strongest leaders. -* **The "1 Highest Conviction" sub-option** is the ultimate momentum play, concentrating all allowed risk into the single strongest trend. -* **Authorship**: Zarattini and Antonacci's names are now front-and-center in the methodology section. - -**Would you like me to add a "Drawdown Overlay" chart so you can compare the risk spikes between the Concentrated (1-ETF) and Broad (All ETFs) sub-options?** + else: + st.info("💡 Adjust settings and click 'Run Analysis'.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index bae2a2ae15f3bdfcb384b84f9673873cf5b0616d..a3bfe754990b0f15ef813b872879b22bc4bf5275 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -3,54 +3,55 @@ import numpy as np import pandas_market_calendars as mcal def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, sub_option): - # 1. Trend Signals & Conviction Scoring + # 1. Trend & Conviction Logic sma_200 = price_df.rolling(200).mean() sma_50 = price_df.rolling(50).mean() - # Conviction Score = How far is the price above the 200 SMA? + # Conviction = Percentage distance above the 200 SMA conviction_score = (price_df / sma_200) - 1 signals = (sma_50 > sma_200).astype(int) - # 2. Individual Asset Volatility + # 2. Risk Metrics returns = price_df.pct_change() asset_vol = returns.rolling(60).std() * np.sqrt(252) - # 3. CONVICTION FILTERING (Sub-Options) + # 3. Apply Sub-Option Concentration if sub_option == "3 Highest Conviction": - # Rank assets by score, keep only top 3 that are ALSO in trend + # Rank daily: 1 is highest conviction ranks = conviction_score.rank(axis=1, ascending=False) signals = ((ranks <= 3) & (signals == 1)).astype(int) elif sub_option == "1 Highest Conviction": - # Rank assets, keep only the top 1 that is ALSO in trend ranks = conviction_score.rank(axis=1, ascending=False) signals = ((ranks <= 1) & (signals == 1)).astype(int) - # "All Trending" remains as is + # Else: "All Trending ETFs" uses the base signals - # 4. Volatility Scaling + # 4. Volatility Target Weighting + # Methodology: Target Vol / Asset Vol, distributed across active signals active_counts = signals.sum(axis=1) - # Inverse vol weight per asset raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).replace([np.inf, -np.inf], 0).fillna(0) final_weights = raw_weights * signals - # 5. Leverage Cap & Cash + # 5. Leverage Cap (1.5x) total_exposure = final_weights.sum(axis=1) scale_factor = total_exposure.apply(lambda x: 1.5/x if x > 1.5 else 1.0) final_weights = final_weights.multiply(scale_factor, axis=0) + # 6. Cash (SOFR) Allocation cash_weight = 1.0 - final_weights.sum(axis=1) - # 6. Performance Calculation + # 7. Portfolio Returns portfolio_ret = (final_weights.shift(1) * returns).sum(axis=1) portfolio_ret += cash_weight.shift(1) * (sofr_series.shift(1) / 252) - # 7. OOS Filtering + # 8. Out-of-Sample Slicing oos_mask = portfolio_ret.index.year >= start_yr equity_curve = (1 + portfolio_ret[oos_mask]).cumprod() bench_curve = (1 + bench_series.pct_change().fillna(0)[oos_mask]).cumprod() - # 8. Stats - dd = (equity_curve / equity_curve.cummax()) - 1 + # Stats ann_ret = portfolio_ret[oos_mask].mean() * 252 + ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) + dd = (equity_curve / equity_curve.cummax()) - 1 # NYSE Calendar nyse = mcal.get_calendar('NYSE') @@ -61,7 +62,7 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, 'equity_curve': equity_curve, 'bench_curve': bench_curve, 'ann_ret': ann_ret, - 'sharpe': (ann_ret - sofr_series.iloc[-1]) / (portfolio_ret[oos_mask].std() * np.sqrt(252)), + 'sharpe': (ann_ret - sofr_series.iloc[-1]) / ann_vol if ann_vol > 0 else 0, 'max_dd': dd.min(), 'next_day': next_day.date(), 'current_weights': final_weights.iloc[-1], diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index e200fc8366c0d4f3ce8e97b3ed3e26c28ec226fa..0e5b7a6519a9e812cd0d2b79b40f1d6975a4eb84 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -10,20 +10,14 @@ if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() with st.sidebar: - st.header("đŸ—‚ī¸ Data Controls") - if st.session_state.master_data is None: - if st.button("🚀 Seed Database (FRED/Stooq)"): - st.session_state.master_data = seed_dataset_from_scratch() - st.rerun() - else: - st.success(f"DB Last Updated: {st.session_state.master_data.index.max().date()}") - if st.button("🔄 Sync Daily Data"): - st.session_state.master_data = sync_incremental_data(st.session_state.master_data) - st.rerun() + st.header("đŸ—‚ī¸ Configuration") + option = st.selectbox("Universe Selection", ("Option A - FI Trend", "Option B - Equity Trend")) - st.divider() - option = st.radio("Asset Universe", ("Option A - FI Trend", "Option B - Equity Trend")) - start_yr = st.slider("Out-of-Sample Start", 2008, 2026, 2018) + # NEW SUB-OPTIONS + sub_option = st.selectbox("Conviction Level", + ("All Trending ETFs", "3 Highest Conviction", "1 Highest Conviction")) + + start_yr = st.slider("OOS Start", 2008, 2026, 2018) vol_target = st.slider("Volatility Target (%)", 5, 20, 12) / 100 run_btn = st.button("🚀 Run Analysis", use_container_width=True, type="primary") @@ -36,50 +30,50 @@ if st.session_state.master_data is not None: results = run_trend_module(st.session_state.master_data[univ], st.session_state.master_data[bench], st.session_state.master_data['SOFR_ANNUAL'], - vol_target, start_yr) + vol_target, start_yr, sub_option) - st.title(f"📈 {option} Performance Report") + st.title(f"📊 {option} - {sub_option}") - # Row 1: Key Metrics (Reordered) + # Metrics m1, m2, m3, m4 = st.columns(4) m1.metric("Annual Return", f"{results['ann_ret']:.1%}") m2.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") - m3.metric("Max Drawdown", f"{results['max_dd_peak']:.1%}") - m4.metric("Current SOFR (Live)", f"{results['current_sofr']:.2%}") + m3.metric("Max Drawdown", f"{results['max_dd']:.1%}") + m4.metric("Current SOFR", f"{results['current_sofr']:.2%}") - # Row 2: Interactive Plotly Chart (Visible Years) + # Chart fig = go.Figure() fig.add_trace(go.Scatter(x=results['equity_curve'].index, y=results['equity_curve'], name='Strategy')) fig.add_trace(go.Scatter(x=results['bench_curve'].index, y=results['bench_curve'], name=f'Benchmark ({bench})')) - fig.update_layout(title="Growth of $1.00 (OOS)", template="plotly_dark", xaxis_title="Timeline") + fig.update_layout(title="OOS Performance", template="plotly_dark") st.plotly_chart(fig, use_container_width=True) - # Row 3: Methodology & Allocations + # Methodology & Target st.divider() col_left, col_right = st.columns([1, 1.5]) with col_left: st.subheader(f"đŸŽ¯ Target Allocation: {results['next_day']}") - weights_df = results['current_weights'][results['current_weights'] > 0.001].to_dict() - weights_df['CASH (SOFR)'] = results['cash_weight'] - - # Clean Table View - final_df = pd.DataFrame.from_dict(weights_df, orient='index', columns=['Weight']) - final_df['Weight'] = final_df['Weight'].apply(lambda x: f"{x:.2%}") - st.table(final_df) + w = results['current_weights'][results['current_weights'] > 0.0001].to_dict() + w['CASH (SOFR)'] = results['cash_weight'] + st.table(pd.DataFrame.from_dict(w, orient='index', columns=['Weight']).style.format("{:.2%}")) with col_right: - st.subheader("📚 Strategy Methodology") + st.subheader("📚 Methodology: Zarattini & Antonacci (2025)") st.markdown(f""" - This engine implements the **'Century of Profitable Trends'** framework (2025 Dow Award): + This strategy implements the **2025 Charles H. Dow Award** framework authored by **Andrea Zarattini** and **Michael Antonacci**. - 1. **Regime Identification**: A dual 50/200-day Simple Moving Average (SMA) filter determines eligibility. Only assets in an uptrend are held. - 2. **Inverse-Volatility Sizing**: Unlike equal weighting, each asset is sized based on its 60-day realized volatility. Lower volatility assets receive higher capital allocations. - 3. **Portfolio Risk Targeting**: The system calculates a total portfolio weight to meet your **{vol_target:.0%} Volatility Target**. - 4. **Cash Scaling (SOFR)**: If the combined risk of the trending assets exceeds the target, or if assets fall out of trend, capital is diverted to **CASH**, earning the live SOFR rate. - 5. **Leverage Management**: Gross exposure is dynamically managed and capped at 1.5x to prevent excessive drawdown during regime shifts. + * **Trend Detection**: Uses a 50/200 SMA dual-filter. + * **Conviction Scoring**: Assets are ranked based on their relative distance from the 200-day trend line. + * **Concentration**: Under **{sub_option}**, the engine filters the universe to only the top-tier trending assets. + * **Risk Sizing**: Allocation is inversely proportional to 60-day volatility. If the selected ETFs cannot safely fill the **{vol_target:.0%}** risk budget, the remainder is held in **CASH (SOFR)**. """) - - else: - st.info("💡 Adjust your risk parameters and click 'Run Analysis' to see the predicted allocations.") + + +### Why this is powerful: +* **The "3 Highest Conviction" sub-option** creates a "Best of the Best" portfolio. Instead of diluting your risk budget across 20 ETFs that are barely in trend, it puts the full 12% risk budget into the 3 strongest leaders. +* **The "1 Highest Conviction" sub-option** is the ultimate momentum play, concentrating all allowed risk into the single strongest trend. +* **Authorship**: Zarattini and Antonacci's names are now front-and-center in the methodology section. + +**Would you like me to add a "Drawdown Overlay" chart so you can compare the risk spikes between the Concentrated (1-ETF) and Broad (All ETFs) sub-options?** diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index 3a52e4e6af718ab97a3984e292bbd4f353884670..bae2a2ae15f3bdfcb384b84f9673873cf5b0616d 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -2,53 +2,57 @@ import pandas as pd import numpy as np import pandas_market_calendars as mcal -def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): - # 1. Signal Logic: Dual SMA Crossover - sma_fast = price_df.rolling(50).mean() - sma_slow = price_df.rolling(200).mean() - signals = (sma_fast > sma_slow).astype(int) +def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr, sub_option): + # 1. Trend Signals & Conviction Scoring + sma_200 = price_df.rolling(200).mean() + sma_50 = price_df.rolling(50).mean() - # 2. Volatility Logic: 60-Day Realized Standard Deviation + # Conviction Score = How far is the price above the 200 SMA? + conviction_score = (price_df / sma_200) - 1 + signals = (sma_50 > sma_200).astype(int) + + # 2. Individual Asset Volatility returns = price_df.pct_change() asset_vol = returns.rolling(60).std() * np.sqrt(252) - # 3. Risk-Budgeted Weighting - # Methodology: Allocation = (Target Vol / Asset Vol) / Number of Active Assets - # This ensures that each trending asset contributes a fixed 'slice' of risk. - active_counts = signals.sum(axis=1) + # 3. CONVICTION FILTERING (Sub-Options) + if sub_option == "3 Highest Conviction": + # Rank assets by score, keep only top 3 that are ALSO in trend + ranks = conviction_score.rank(axis=1, ascending=False) + signals = ((ranks <= 3) & (signals == 1)).astype(int) + elif sub_option == "1 Highest Conviction": + # Rank assets, keep only the top 1 that is ALSO in trend + ranks = conviction_score.rank(axis=1, ascending=False) + signals = ((ranks <= 1) & (signals == 1)).astype(int) + # "All Trending" remains as is - # Weight per asset: Target Vol divided by Asset Vol, then distributed among active trends - raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).fillna(0) + # 4. Volatility Scaling + active_counts = signals.sum(axis=1) + # Inverse vol weight per asset + raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).replace([np.inf, -np.inf], 0).fillna(0) final_weights = raw_weights * signals - # 4. Leverage Cap & Cash Logic - # We cap total gross exposure at 1.5x (150%) to prevent extreme tail risk + # 5. Leverage Cap & Cash total_exposure = final_weights.sum(axis=1) scale_factor = total_exposure.apply(lambda x: 1.5/x if x > 1.5 else 1.0) final_weights = final_weights.multiply(scale_factor, axis=0) - # Recalculate exposure after capping - final_exposure = final_weights.sum(axis=1) - cash_weight = 1.0 - final_exposure + cash_weight = 1.0 - final_weights.sum(axis=1) - # 5. Returns: Asset Performance + Cash (SOFR) Interest - # If exposure is < 100%, the remainder earns SOFR interest + # 6. Performance Calculation portfolio_ret = (final_weights.shift(1) * returns).sum(axis=1) portfolio_ret += cash_weight.shift(1) * (sofr_series.shift(1) / 252) - bench_returns = bench_series.pct_change().fillna(0) - - # 6. OOS Performance Slicing + # 7. OOS Filtering oos_mask = portfolio_ret.index.year >= start_yr equity_curve = (1 + portfolio_ret[oos_mask]).cumprod() - bench_curve = (1 + bench_returns[oos_mask]).cumprod() + bench_curve = (1 + bench_series.pct_change().fillna(0)[oos_mask]).cumprod() - # 7. Drawdown & Stats - dd_series = (equity_curve / equity_curve.cummax()) - 1 + # 8. Stats + dd = (equity_curve / equity_curve.cummax()) - 1 ann_ret = portfolio_ret[oos_mask].mean() * 252 - ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) - # NYSE Calendar for Next Session + # NYSE Calendar nyse = mcal.get_calendar('NYSE') last_dt = price_df.index[-1] next_day = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)).index[1] @@ -57,9 +61,8 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): 'equity_curve': equity_curve, 'bench_curve': bench_curve, 'ann_ret': ann_ret, - 'sharpe': (ann_ret - sofr_series.iloc[-1]) / ann_vol if ann_vol > 0 else 0, - 'max_dd_peak': dd_series.min(), - 'avg_daily_dd': dd_series.mean(), + 'sharpe': (ann_ret - sofr_series.iloc[-1]) / (portfolio_ret[oos_mask].std() * np.sqrt(252)), + 'max_dd': dd.min(), 'next_day': next_day.date(), 'current_weights': final_weights.iloc[-1], 'cash_weight': cash_weight.iloc[-1], diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index 0d4e13c6b83f702ffe2c2fc6463cf5037b36ede8..3a52e4e6af718ab97a3984e292bbd4f353884670 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -3,49 +3,65 @@ import numpy as np import pandas_market_calendars as mcal def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): - # 1. Full-period math for indicators + # 1. Signal Logic: Dual SMA Crossover sma_fast = price_df.rolling(50).mean() sma_slow = price_df.rolling(200).mean() signals = (sma_fast > sma_slow).astype(int) + # 2. Volatility Logic: 60-Day Realized Standard Deviation returns = price_df.pct_change() - realized_vol = returns.rolling(60).std() * np.sqrt(252) - weights = (target_vol / realized_vol).fillna(0).clip(upper=1.5) + asset_vol = returns.rolling(60).std() * np.sqrt(252) - # 2. Strategy Returns - asset_ret = (signals.shift(1) * weights.shift(1) * returns).mean(axis=1) - cash_pct = 1 - signals.mean(axis=1) - strat_returns = asset_ret + (cash_pct.shift(1) * (sofr_series.shift(1) / 252)) - bench_returns = bench_series.pct_change().fillna(0) + # 3. Risk-Budgeted Weighting + # Methodology: Allocation = (Target Vol / Asset Vol) / Number of Active Assets + # This ensures that each trending asset contributes a fixed 'slice' of risk. + active_counts = signals.sum(axis=1) + + # Weight per asset: Target Vol divided by Asset Vol, then distributed among active trends + raw_weights = (target_vol / asset_vol).divide(active_counts, axis=0).fillna(0) + final_weights = raw_weights * signals - # 3. Slice for OOS Period - oos_mask = strat_returns.index.year >= start_yr - oos_strat = strat_returns[oos_mask] - oos_bench = bench_returns[oos_mask] + # 4. Leverage Cap & Cash Logic + # We cap total gross exposure at 1.5x (150%) to prevent extreme tail risk + total_exposure = final_weights.sum(axis=1) + scale_factor = total_exposure.apply(lambda x: 1.5/x if x > 1.5 else 1.0) + final_weights = final_weights.multiply(scale_factor, axis=0) - equity_curve = (1 + oos_strat).cumprod() - bench_curve = (1 + oos_bench).cumprod() + # Recalculate exposure after capping + final_exposure = final_weights.sum(axis=1) + cash_weight = 1.0 - final_exposure - # 4. Drawdowns - hwm = equity_curve.cummax() - dd_series = (equity_curve / hwm) - 1 + # 5. Returns: Asset Performance + Cash (SOFR) Interest + # If exposure is < 100%, the remainder earns SOFR interest + portfolio_ret = (final_weights.shift(1) * returns).sum(axis=1) + portfolio_ret += cash_weight.shift(1) * (sofr_series.shift(1) / 252) + + bench_returns = bench_series.pct_change().fillna(0) - # 5. Next Day Trading Date + # 6. OOS Performance Slicing + oos_mask = portfolio_ret.index.year >= start_yr + equity_curve = (1 + portfolio_ret[oos_mask]).cumprod() + bench_curve = (1 + bench_returns[oos_mask]).cumprod() + + # 7. Drawdown & Stats + dd_series = (equity_curve / equity_curve.cummax()) - 1 + ann_ret = portfolio_ret[oos_mask].mean() * 252 + ann_vol = portfolio_ret[oos_mask].std() * np.sqrt(252) + + # NYSE Calendar for Next Session nyse = mcal.get_calendar('NYSE') last_dt = price_df.index[-1] - sched = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)) - next_day = sched.index[1] if len(sched) > 1 else sched.index[0] - - ann_ret = oos_strat.mean() * 252 - ann_vol = oos_strat.std() * np.sqrt(252) + next_day = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)).index[1] return { 'equity_curve': equity_curve, 'bench_curve': bench_curve, - 'sharpe': (ann_ret - 0.03) / ann_vol if ann_vol > 0 else 0, 'ann_ret': ann_ret, + 'sharpe': (ann_ret - sofr_series.iloc[-1]) / ann_vol if ann_vol > 0 else 0, 'max_dd_peak': dd_series.min(), 'avg_daily_dd': dd_series.mean(), 'next_day': next_day.date(), - 'current_signals': signals.iloc[-1] + 'current_weights': final_weights.iloc[-1], + 'cash_weight': cash_weight.iloc[-1], + 'current_sofr': sofr_series.iloc[-1] } diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index ccb0cfb6b9cd162cea9497db63905176eb4e30db..e200fc8366c0d4f3ce8e97b3ed3e26c28ec226fa 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,95 +1,85 @@ import streamlit as st import pandas as pd -import numpy as np +import plotly.graph_objects as go from data.loader import load_from_hf, seed_dataset_from_scratch, sync_incremental_data, X_EQUITY_TICKERS, FI_TICKERS from engine.trend_engine import run_trend_module -st.set_page_config(layout="wide", page_title="P2 Strategy Suite | 2025 Dow Award Edition") +st.set_page_config(layout="wide", page_title="P2 Strategy Suite") -# --- SAFE SESSION INITIALIZATION --- if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() -# --- SIDEBAR UI --- with st.sidebar: - st.header("đŸ—‚ī¸ Data Management") + st.header("đŸ—‚ī¸ Data Controls") if st.session_state.master_data is None: - st.error("Dataset not found.") - if st.button("🚀 Seed Database (2008-2026)", use_container_width=True): + if st.button("🚀 Seed Database (FRED/Stooq)"): st.session_state.master_data = seed_dataset_from_scratch() st.rerun() else: - last_dt = pd.to_datetime(st.session_state.master_data.index).max() - st.success(f"Database Active: {last_dt.date()}") - if st.button("🔄 Sync Daily Data", use_container_width=True): + st.success(f"DB Last Updated: {st.session_state.master_data.index.max().date()}") + if st.button("🔄 Sync Daily Data"): st.session_state.master_data = sync_incremental_data(st.session_state.master_data) st.rerun() st.divider() - st.header("âš™ī¸ Strategy Settings") - option = st.radio("Strategy Selection", ("Option A - FI Trend", "Option B - Equity Trend")) - start_yr = st.slider("OOS Start Year", 2008, 2026, 2018) - vol_target = st.slider("Ann. Vol Target (%)", 5, 25, 12) / 100 + option = st.radio("Asset Universe", ("Option A - FI Trend", "Option B - Equity Trend")) + start_yr = st.slider("Out-of-Sample Start", 2008, 2026, 2018) + vol_target = st.slider("Volatility Target (%)", 5, 20, 12) / 100 run_btn = st.button("🚀 Run Analysis", use_container_width=True, type="primary") -# --- MAIN OUTPUT UI --- if st.session_state.master_data is not None: if run_btn: - with st.spinner("Analyzing Market Regimes..."): - # 1. Setup Universe and Benchmark - is_fi = "Option A" in option - univ = FI_TICKERS if is_fi else X_EQUITY_TICKERS - bench_ticker = "AGG" if is_fi else "SPY" - - # 2. Filter Data (Using Start Year as OOS boundary) - # The engine uses data prior to start_yr for signal lookback (Training/Buffer) - df = st.session_state.master_data - - # 3. Execute Engine - results = run_trend_module(df[univ], df[bench_ticker], df['SOFR_ANNUAL'], vol_target, start_yr) - - # 4. KPI Header - st.title(f"📈 {option} Performance vs {bench_ticker}") - m1, m2, m3, m4 = st.columns(4) - m1.metric("OOS Sharpe", f"{results['sharpe']:.2f}") - m2.metric("Ann. Return", f"{results['ann_ret']:.1%}") - m3.metric("Peak-to-Trough DD", f"{results['max_dd_peak']:.1%}") - m4.metric("Avg Daily DD", f"{results['avg_daily_dd']:.2%}") + is_fi = "Option A" in option + univ = FI_TICKERS if is_fi else X_EQUITY_TICKERS + bench = "AGG" if is_fi else "SPY" + + results = run_trend_module(st.session_state.master_data[univ], + st.session_state.master_data[bench], + st.session_state.master_data['SOFR_ANNUAL'], + vol_target, start_yr) + + st.title(f"📈 {option} Performance Report") + + # Row 1: Key Metrics (Reordered) + m1, m2, m3, m4 = st.columns(4) + m1.metric("Annual Return", f"{results['ann_ret']:.1%}") + m2.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") + m3.metric("Max Drawdown", f"{results['max_dd_peak']:.1%}") + m4.metric("Current SOFR (Live)", f"{results['current_sofr']:.2%}") - # 5. Equity Curve Chart - chart_df = pd.DataFrame({ - "Strategy Portfolio": results['equity_curve'], - f"Benchmark ({bench_ticker})": results['bench_curve'] - }) - st.subheader("Cumulative Growth of $1.00 (Out-of-Sample)") - st.line_chart(chart_df) + # Row 2: Interactive Plotly Chart (Visible Years) + fig = go.Figure() + fig.add_trace(go.Scatter(x=results['equity_curve'].index, y=results['equity_curve'], name='Strategy')) + fig.add_trace(go.Scatter(x=results['bench_curve'].index, y=results['bench_curve'], name=f'Benchmark ({bench})')) + fig.update_layout(title="Growth of $1.00 (OOS)", template="plotly_dark", xaxis_title="Timeline") + st.plotly_chart(fig, use_container_width=True) - # 6. Actionable Allocation (Next Trading Day) - st.divider() - c1, c2 = st.columns([1, 2]) - with c1: - st.subheader("📅 Next Trading Session") - st.info(f"**NYSE Market Date:** {results['next_day']}\n\n**Action:** Execute at Open") - with c2: - st.subheader("đŸŽ¯ Required Allocation") - active = results['current_signals'][results['current_signals'] > 0].index.tolist() - if active: - st.success(f"**Long Positions:** {', '.join(active)}") - else: - st.warning("âš–ī¸ **Position:** 100% CASH (Market Neutral)") + # Row 3: Methodology & Allocations + st.divider() + col_left, col_right = st.columns([1, 1.5]) + + with col_left: + st.subheader(f"đŸŽ¯ Target Allocation: {results['next_day']}") + weights_df = results['current_weights'][results['current_weights'] > 0.001].to_dict() + weights_df['CASH (SOFR)'] = results['cash_weight'] + + # Clean Table View + final_df = pd.DataFrame.from_dict(weights_df, orient='index', columns=['Weight']) + final_df['Weight'] = final_df['Weight'].apply(lambda x: f"{x:.2%}") + st.table(final_df) - # 7. Methodology Footer - st.divider() - with st.expander("📚 Methodology & 2025 Dow Award Reference"): - st.markdown(""" - ### A Century of Profitable Trends (Zarattini & Antonacci, 2025) - This model implements the framework from the 2025 Charles H. Dow Award winning paper: - * **Regime Filter:** Dual SMA logic (50/200 crossover) proxying for Keltner/Donchian channels. - * **Volatility Targeting:** Positions sized by $Weight = \sigma_{target} / \sigma_{realized}$, capped at 1.5x. - * **Benchmarking:** Equity trends are compared to SPY; Fixed Income to AGG. - * **OOS Testing:** The analysis shown above represents the **Out-of-Sample** period. Data prior to the start year is used solely for initial indicator 'burn-in'. - """) + with col_right: + st.subheader("📚 Strategy Methodology") + st.markdown(f""" + This engine implements the **'Century of Profitable Trends'** framework (2025 Dow Award): + + 1. **Regime Identification**: A dual 50/200-day Simple Moving Average (SMA) filter determines eligibility. Only assets in an uptrend are held. + 2. **Inverse-Volatility Sizing**: Unlike equal weighting, each asset is sized based on its 60-day realized volatility. Lower volatility assets receive higher capital allocations. + 3. **Portfolio Risk Targeting**: The system calculates a total portfolio weight to meet your **{vol_target:.0%} Volatility Target**. + 4. **Cash Scaling (SOFR)**: If the combined risk of the trending assets exceeds the target, or if assets fall out of trend, capital is diverted to **CASH**, earning the live SOFR rate. + 5. **Leverage Management**: Gross exposure is dynamically managed and capped at 1.5x to prevent excessive drawdown during regime shifts. + """) + + else: - st.info("💡 Adjust your parameters in the sidebar and click **'Run Analysis'**.") -else: - st.warning("👈 Please click 'Seed Database' to initialize historical data.") + st.info("💡 Adjust your risk parameters and click 'Run Analysis' to see the predicted allocations.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index 49f65b68d854511f5490106f41db4eb253c9b6cf..0d4e13c6b83f702ffe2c2fc6463cf5037b36ede8 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -3,7 +3,7 @@ import numpy as np import pandas_market_calendars as mcal def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): - # 1. Full period calculations for signals (Training + OOS) + # 1. Full-period math for indicators sma_fast = price_df.rolling(50).mean() sma_slow = price_df.rolling(200).mean() signals = (sma_fast > sma_slow).astype(int) @@ -18,7 +18,7 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): strat_returns = asset_ret + (cash_pct.shift(1) * (sofr_series.shift(1) / 252)) bench_returns = bench_series.pct_change().fillna(0) - # 3. Filter for Out-of-Sample (OOS) Period + # 3. Slice for OOS Period oos_mask = strat_returns.index.year >= start_yr oos_strat = strat_returns[oos_mask] oos_bench = bench_returns[oos_mask] @@ -26,11 +26,11 @@ def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): equity_curve = (1 + oos_strat).cumprod() bench_curve = (1 + oos_bench).cumprod() - # 4. Drawdown Stats + # 4. Drawdowns hwm = equity_curve.cummax() dd_series = (equity_curve / hwm) - 1 - # 5. NYSE Next Day + # 5. Next Day Trading Date nyse = mcal.get_calendar('NYSE') last_dt = price_df.index[-1] sched = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index a2d9aa1dcc7eeeed886de4a229f73cd58f347943..02059f1978476193b6dbfec9e6c8492e5a322828 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -12,36 +12,45 @@ FILENAME = "market_data.csv" X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] +def get_safe_token(): + """Bypasses Streamlit's hard-fail on missing secrets.toml by using environment fallback.""" + try: + # Try Streamlit secrets first + return st.secrets["HF_TOKEN"] + except Exception: + # Standard environment variable fallback (How HF actually stores them) + return os.getenv("HF_TOKEN") + def load_from_hf(): + token = get_safe_token() + if not token: return None try: - token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") - if not token: return None path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) return pd.read_csv(path, index_col=0, parse_dates=True) except: return None def seed_dataset_from_scratch(): - # Include benchmarks SPY and AGG + # Include benchmarks for comparison logic tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) master_df = pd.DataFrame() status = st.empty() progress = st.progress(0) for i, t in enumerate(tickers): - status.text(f"Fetching {t} from Stooq...") + status.text(f"đŸ›°ī¸ Fetching {t} from Stooq...") try: data = web.DataReader(f"{t}.US", 'stooq', start='2008-01-01') if not data.empty: master_df[t] = data['Close'].sort_index() - time.sleep(0.6) + time.sleep(0.7) # Polite delay except: try: master_df[t] = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] except: pass progress.progress((i + 1) / len(tickers)) - # Add SOFR Rate + # Add SOFR Rate (Cash Interest) try: sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() master_df['SOFR_ANNUAL'] = sofr / 100 @@ -64,6 +73,9 @@ def sync_incremental_data(df): return combined def upload_to_hf(path): + token = get_safe_token() + if not token: + st.error("❌ Cannot upload: HF_TOKEN is missing from Space Secrets.") + return api = HfApi() - token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index d0649323f62d86c22f1dd7db8330c7d9afe59886..49f65b68d854511f5490106f41db4eb253c9b6cf 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -2,12 +2,8 @@ import pandas as pd import numpy as np import pandas_market_calendars as mcal -def run_trend_module(price_df, benchmark_df, sofr_series, target_vol=0.12): - """ - Enhanced Engine for 2025 Dow Award Logic. - Includes Dual Drawdowns and Benchmark Comparison. - """ - # 1. Signals & Weights +def run_trend_module(price_df, bench_series, sofr_series, target_vol, start_yr): + # 1. Full period calculations for signals (Training + OOS) sma_fast = price_df.rolling(50).mean() sma_slow = price_df.rolling(200).mean() signals = (sma_fast > sma_slow).astype(int) @@ -16,43 +12,40 @@ def run_trend_module(price_df, benchmark_df, sofr_series, target_vol=0.12): realized_vol = returns.rolling(60).std() * np.sqrt(252) weights = (target_vol / realized_vol).fillna(0).clip(upper=1.5) - # 2. Returns Calculation - # Strategy + # 2. Strategy Returns asset_ret = (signals.shift(1) * weights.shift(1) * returns).mean(axis=1) cash_pct = 1 - signals.mean(axis=1) strat_returns = asset_ret + (cash_pct.shift(1) * (sofr_series.shift(1) / 252)) + bench_returns = bench_series.pct_change().fillna(0) - # Benchmark (Buy & Hold) - bench_returns = benchmark_df.pct_change().fillna(0) + # 3. Filter for Out-of-Sample (OOS) Period + oos_mask = strat_returns.index.year >= start_yr + oos_strat = strat_returns[oos_mask] + oos_bench = bench_returns[oos_mask] - # Equity Curves - equity_curve = (1 + strat_returns).cumprod() - bench_curve = (1 + bench_returns).cumprod() + equity_curve = (1 + oos_strat).cumprod() + bench_curve = (1 + oos_bench).cumprod() - # 3. Drawdown Calculations - def get_dd_stats(curve): - hwm = curve.cummax() - dd = (curve / hwm) - 1 - return dd.min(), dd # Max DD and the full DD series + # 4. Drawdown Stats + hwm = equity_curve.cummax() + dd_series = (equity_curve / hwm) - 1 - max_dd_peak, dd_series = get_dd_stats(equity_curve) - - # 4. Next Trading Day & Allocations (NYSE Calendar) + # 5. NYSE Next Day nyse = mcal.get_calendar('NYSE') - last_date = price_df.index[-1] - next_day = nyse.valid_days(start_date=last_date + pd.Timedelta(days=1), end_date=last_date + pd.Timedelta(days=10))[0] + last_dt = price_df.index[-1] + sched = nyse.schedule(start_date=last_dt, end_date=last_dt + pd.Timedelta(days=10)) + next_day = sched.index[1] if len(sched) > 1 else sched.index[0] - # Current Allocations (Based on most recent signals) - current_signals = signals.iloc[-1] - active_assets = current_signals[current_signals > 0].index.tolist() + ann_ret = oos_strat.mean() * 252 + ann_vol = oos_strat.std() * np.sqrt(252) return { 'equity_curve': equity_curve, 'bench_curve': bench_curve, - 'strat_ret_series': strat_returns, - 'max_dd_peak': max_dd_peak, - 'dd_series': dd_series, - 'next_trading_day': next_day.date(), - 'active_assets': active_assets, - 'signals': current_signals + 'sharpe': (ann_ret - 0.03) / ann_vol if ann_vol > 0 else 0, + 'ann_ret': ann_ret, + 'max_dd_peak': dd_series.min(), + 'avg_daily_dd': dd_series.mean(), + 'next_day': next_day.date(), + 'current_signals': signals.iloc[-1] } diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index 6395d603107a5909740f065940e42e935ae3ed41..a2d9aa1dcc7eeeed886de4a229f73cd58f347943 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -9,86 +9,61 @@ import streamlit as st REPO_ID = "P2SAMAPA/etf_trend_data" FILENAME = "market_data.csv" -# The 27 Equity X-ETFs and 15 FI ETFs from the 2025 Paper X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] -def get_hf_token(): - """Safely retrieves the token from secrets or environment.""" - try: - return st.secrets["HF_TOKEN"] - except: - return os.getenv("HF_TOKEN") - def load_from_hf(): - """Reads dataset from Hugging Face if it exists.""" - token = get_hf_token() - if not token: - return None try: + token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") + if not token: return None path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) return pd.read_csv(path, index_col=0, parse_dates=True) except: return None def seed_dataset_from_scratch(): - """Initial download of 18 years of data using Stooq primarily.""" + # Include benchmarks SPY and AGG tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) master_df = pd.DataFrame() - status = st.empty() - progress_bar = st.progress(0) + progress = st.progress(0) - for i, ticker in enumerate(tickers): - status.text(f"Fetching {ticker} from Stooq...") + for i, t in enumerate(tickers): + status.text(f"Fetching {t} from Stooq...") try: - # Stooq primary (requires .US suffix for ETFs) - data = web.DataReader(f"{ticker}.US", 'stooq', start='2008-01-01') + data = web.DataReader(f"{t}.US", 'stooq', start='2008-01-01') if not data.empty: - master_df[ticker] = data['Close'].sort_index() - time.sleep(0.6) + master_df[t] = data['Close'].sort_index() + time.sleep(0.6) except: - # YFinance fallback if Stooq fails for a ticker try: - yf_data = yf.download(ticker, start="2008-01-01", progress=False)['Adj Close'] - master_df[ticker] = yf_data - except: - pass - progress_bar.progress((i + 1) / len(tickers)) + master_df[t] = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] + except: pass + progress.progress((i + 1) / len(tickers)) - # Add SOFR Rate (Cash interest) + # Add SOFR Rate try: sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() master_df['SOFR_ANNUAL'] = sofr / 100 except: - master_df['SOFR_ANNUAL'] = 0.045 # Conservative proxy + master_df['SOFR_ANNUAL'] = 0.045 master_df = master_df.sort_index().ffill() master_df.to_csv(FILENAME) - upload_to_hf(FILENAME) return master_df -def sync_incremental_data(df_existing): - """Updates only new data since last index date using YFinance for speed.""" - last_date = pd.to_datetime(df_existing.index).max() +def sync_incremental_data(df): + last_date = df.index.max() tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] - combined = pd.concat([df_existing, new_data]) - combined = combined[~combined.index.duplicated(keep='last')].sort_index() - + combined = pd.concat([df, new_data]).sort_index() + combined = combined[~combined.index.duplicated(keep='last')] combined.to_csv(FILENAME) upload_to_hf(FILENAME) return combined def upload_to_hf(path): api = HfApi() - token = get_hf_token() - api.upload_file( - path_or_fileobj=path, - path_in_repo=FILENAME, - repo_id=REPO_ID, - repo_type="dataset", - token=token - ) + token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") + api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=token) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index b90e73874ec136083460d9ad203ac9edbd59b4fe..ccb0cfb6b9cd162cea9497db63905176eb4e30db 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,68 +1,95 @@ import streamlit as st +import pandas as pd +import numpy as np from data.loader import load_from_hf, seed_dataset_from_scratch, sync_incremental_data, X_EQUITY_TICKERS, FI_TICKERS from engine.trend_engine import run_trend_module -st.set_page_config(layout="wide", page_title="P2 Strategy Suite") +st.set_page_config(layout="wide", page_title="P2 Strategy Suite | 2025 Dow Award Edition") -# --- INITIALIZATION --- +# --- SAFE SESSION INITIALIZATION --- if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() -# --- SIDEBAR: DATA CONTROLS --- +# --- SIDEBAR UI --- with st.sidebar: st.header("đŸ—‚ī¸ Data Management") if st.session_state.master_data is None: - st.error("No dataset detected.") + st.error("Dataset not found.") if st.button("🚀 Seed Database (2008-2026)", use_container_width=True): st.session_state.master_data = seed_dataset_from_scratch() st.rerun() else: - last_dt = st.session_state.master_data.index.max() + last_dt = pd.to_datetime(st.session_state.master_data.index).max() st.success(f"Database Active: {last_dt.date()}") - if st.button("🔄 Sync New Data", use_container_width=True): + if st.button("🔄 Sync Daily Data", use_container_width=True): st.session_state.master_data = sync_incremental_data(st.session_state.master_data) st.rerun() st.divider() st.header("âš™ī¸ Strategy Settings") - option = st.radio("Universe Selection", ("Option A - FI Trend", "Option B - Equity Trend")) - start_yr = st.slider("Backtest Start Year", 2008, 2026, 2015) - vol_target = st.slider("Target Vol (%)", 5, 20, 12) / 100 - - st.divider() - run_btn = st.button("🚀 Run Strategy Analysis", use_container_width=True, type="primary") + option = st.radio("Strategy Selection", ("Option A - FI Trend", "Option B - Equity Trend")) + start_yr = st.slider("OOS Start Year", 2008, 2026, 2018) + vol_target = st.slider("Ann. Vol Target (%)", 5, 25, 12) / 100 + run_btn = st.button("🚀 Run Analysis", use_container_width=True, type="primary") -# --- MAIN PAGE: DISPLAY --- +# --- MAIN OUTPUT UI --- if st.session_state.master_data is not None: if run_btn: - with st.spinner("Crunching data..."): - # Universe Selection - univ = FI_TICKERS if "Option A" in option else X_EQUITY_TICKERS - # Slice by date - df = st.session_state.master_data[st.session_state.master_data.index.year >= start_yr] + with st.spinner("Analyzing Market Regimes..."): + # 1. Setup Universe and Benchmark + is_fi = "Option A" in option + univ = FI_TICKERS if is_fi else X_EQUITY_TICKERS + bench_ticker = "AGG" if is_fi else "SPY" - # Execute Engine - results = run_trend_module(df[univ], df['SOFR_ANNUAL'], vol_target) + # 2. Filter Data (Using Start Year as OOS boundary) + # The engine uses data prior to start_yr for signal lookback (Training/Buffer) + df = st.session_state.master_data - # Show Metrics - st.title(f"📊 {option} Performance Report") - m1, m2, m3 = st.columns(3) - m1.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") - m2.metric("Annual Return", f"{results['ann_ret']:.1%}") - m3.metric("Max Drawdown", f"{results['max_dd']:.1%}") + # 3. Execute Engine + results = run_trend_module(df[univ], df[bench_ticker], df['SOFR_ANNUAL'], vol_target, start_yr) - # Equity Curve - st.subheader("Cumulative Growth (vs Cash)") - st.line_chart(results['equity_curve']) - - # Allocation Check + # 4. KPI Header + st.title(f"📈 {option} Performance vs {bench_ticker}") + m1, m2, m3, m4 = st.columns(4) + m1.metric("OOS Sharpe", f"{results['sharpe']:.2f}") + m2.metric("Ann. Return", f"{results['ann_ret']:.1%}") + m3.metric("Peak-to-Trough DD", f"{results['max_dd_peak']:.1%}") + m4.metric("Avg Daily DD", f"{results['avg_daily_dd']:.2%}") + + # 5. Equity Curve Chart + chart_df = pd.DataFrame({ + "Strategy Portfolio": results['equity_curve'], + f"Benchmark ({bench_ticker})": results['bench_curve'] + }) + st.subheader("Cumulative Growth of $1.00 (Out-of-Sample)") + st.line_chart(chart_df) + + # 6. Actionable Allocation (Next Trading Day) st.divider() - st.subheader("Current Market Status") - active_assets = results['current_signals'][results['current_signals'] > 0].index.tolist() - st.write(f"**In-Trend Assets:** {', '.join(active_assets) if active_assets else 'All Cash'}") - + c1, c2 = st.columns([1, 2]) + with c1: + st.subheader("📅 Next Trading Session") + st.info(f"**NYSE Market Date:** {results['next_day']}\n\n**Action:** Execute at Open") + with c2: + st.subheader("đŸŽ¯ Required Allocation") + active = results['current_signals'][results['current_signals'] > 0].index.tolist() + if active: + st.success(f"**Long Positions:** {', '.join(active)}") + else: + st.warning("âš–ī¸ **Position:** 100% CASH (Market Neutral)") + + # 7. Methodology Footer + st.divider() + with st.expander("📚 Methodology & 2025 Dow Award Reference"): + st.markdown(""" + ### A Century of Profitable Trends (Zarattini & Antonacci, 2025) + This model implements the framework from the 2025 Charles H. Dow Award winning paper: + * **Regime Filter:** Dual SMA logic (50/200 crossover) proxying for Keltner/Donchian channels. + * **Volatility Targeting:** Positions sized by $Weight = \sigma_{target} / \sigma_{realized}$, capped at 1.5x. + * **Benchmarking:** Equity trends are compared to SPY; Fixed Income to AGG. + * **OOS Testing:** The analysis shown above represents the **Out-of-Sample** period. Data prior to the start year is used solely for initial indicator 'burn-in'. + """) else: - st.title("Welcome to the 2025 Trend Suite") - st.info("👈 Use the sidebar to manage your data and click 'Run Strategy Analysis' to begin.") + st.info("💡 Adjust your parameters in the sidebar and click **'Run Analysis'**.") else: - st.warning("Please initialize the database using the 'Seed' button in the sidebar.") + st.warning("👈 Please click 'Seed Database' to initialize historical data.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index aceee11eade2c4eab9ab6fc13fa316d87640e197..d0649323f62d86c22f1dd7db8330c7d9afe59886 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -1,47 +1,58 @@ import pandas as pd import numpy as np +import pandas_market_calendars as mcal -def run_trend_module(price_df, sofr_series, target_vol=0.12): +def run_trend_module(price_df, benchmark_df, sofr_series, target_vol=0.12): """ - Implements 2025 Dow Award Logic. + Enhanced Engine for 2025 Dow Award Logic. + Includes Dual Drawdowns and Benchmark Comparison. """ - # 1. Dual-Trend Signal (Fast vs Slow SMA) + # 1. Signals & Weights sma_fast = price_df.rolling(50).mean() sma_slow = price_df.rolling(200).mean() - # Signal is 1 if in trend, 0 if cash signals = (sma_fast > sma_slow).astype(int) - # 2. Volatility Targeting (Inverse Vol Sizing) returns = price_df.pct_change() realized_vol = returns.rolling(60).std() * np.sqrt(252) - # Weights = Target Vol / Realized Vol - weights = (target_vol / realized_vol).fillna(0) - weights = weights.clip(upper=1.5) # Cap leverage at 150% + weights = (target_vol / realized_vol).fillna(0).clip(upper=1.5) - # 3. Portfolio Returns - # Position = Signal * Weight - asset_returns = (signals.shift(1) * weights.shift(1) * returns).mean(axis=1) + # 2. Returns Calculation + # Strategy + asset_ret = (signals.shift(1) * weights.shift(1) * returns).mean(axis=1) + cash_pct = 1 - signals.mean(axis=1) + strat_returns = asset_ret + (cash_pct.shift(1) * (sofr_series.shift(1) / 252)) - # 4. Interest on Cash (SOFR) - # If signals are 0 (in cash), we earn SOFR - cash_percentage = 1 - signals.mean(axis=1) - interest_returns = (cash_percentage.shift(1) * (sofr_series.shift(1) / 252)) + # Benchmark (Buy & Hold) + bench_returns = benchmark_df.pct_change().fillna(0) - total_returns = asset_returns + interest_returns - equity_curve = (1 + total_returns).fillna(0).cumprod() + # Equity Curves + equity_curve = (1 + strat_returns).cumprod() + bench_curve = (1 + bench_returns).cumprod() - # 5. Metrics - ann_ret = total_returns.mean() * 252 - ann_vol = total_returns.std() * np.sqrt(252) - sharpe = (ann_ret - 0.035) / ann_vol if ann_vol > 0 else 0 + # 3. Drawdown Calculations + def get_dd_stats(curve): + hwm = curve.cummax() + dd = (curve / hwm) - 1 + return dd.min(), dd # Max DD and the full DD series - dd = equity_curve / equity_curve.cummax() - 1 - max_dd = dd.min() + max_dd_peak, dd_series = get_dd_stats(equity_curve) + + # 4. Next Trading Day & Allocations (NYSE Calendar) + nyse = mcal.get_calendar('NYSE') + last_date = price_df.index[-1] + next_day = nyse.valid_days(start_date=last_date + pd.Timedelta(days=1), end_date=last_date + pd.Timedelta(days=10))[0] + + # Current Allocations (Based on most recent signals) + current_signals = signals.iloc[-1] + active_assets = current_signals[current_signals > 0].index.tolist() return { 'equity_curve': equity_curve, - 'sharpe': sharpe, - 'ann_ret': ann_ret, - 'max_dd': max_dd, - 'current_signals': signals.iloc[-1] + 'bench_curve': bench_curve, + 'strat_ret_series': strat_returns, + 'max_dd_peak': max_dd_peak, + 'dd_series': dd_series, + 'next_trading_day': next_day.date(), + 'active_assets': active_assets, + 'signals': current_signals } diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py index 3dccd952718ee84e244b96c41ea568c2f78e73da..aceee11eade2c4eab9ab6fc13fa316d87640e197 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -1,40 +1,47 @@ -import numpy as np import pandas as pd +import numpy as np -def run_trend_module(prices, daily_sofr, vol_target): - # 1. Dual-Trend Signal - d_high = prices.rolling(20).max() - sma = prices.rolling(20).mean() - atr = (prices.rolling(20).max() - prices.rolling(20).min()) / 2 - k_upper = sma + (2 * atr) - - entry_band = np.minimum(d_high, k_upper) - signals = (prices > entry_band.shift(1)).astype(int) - - # 2. Risk Parity Position Sizing - returns = prices.pct_change() - realized_vol = returns.rolling(21).std() * np.sqrt(252) - - n = len(prices.columns) - # Target weight = (Target Vol / Total Assets) / Individual Asset Vol - target_weights = (vol_target / n) / realized_vol.shift(1) - - # 3. Strategy Returns (Positions + SOFR on Cash) - pos_rets = (signals.shift(1) * target_weights.shift(1) * returns).sum(axis=1) - weight_used = (signals.shift(1) * target_weights.shift(1)).sum(axis=1) - cash_rets = (1 - weight_used).clip(0, 1) * (daily_sofr / 252) - - strat_rets = pos_rets + cash_rets - equity_curve = (1 + strat_rets).fillna(0).cumprod() - - # 4. Target Allocation for Tomorrow - tomorrow_sig = (prices.iloc[-1] > entry_band.iloc[-1]).astype(int) - tomorrow_w = (vol_target / n) / realized_vol.iloc[-1] - - alloc = pd.DataFrame({ - "Ticker": prices.columns, - "Signal": ["LONG" if s == 1 else "CASH" for s in tomorrow_sig], - "Weight (%)": (tomorrow_sig * tomorrow_w * 100).round(2) - }) - - return {"curve": equity_curve, "alloc": alloc} +def run_trend_module(price_df, sofr_series, target_vol=0.12): + """ + Implements 2025 Dow Award Logic. + """ + # 1. Dual-Trend Signal (Fast vs Slow SMA) + sma_fast = price_df.rolling(50).mean() + sma_slow = price_df.rolling(200).mean() + # Signal is 1 if in trend, 0 if cash + signals = (sma_fast > sma_slow).astype(int) + + # 2. Volatility Targeting (Inverse Vol Sizing) + returns = price_df.pct_change() + realized_vol = returns.rolling(60).std() * np.sqrt(252) + # Weights = Target Vol / Realized Vol + weights = (target_vol / realized_vol).fillna(0) + weights = weights.clip(upper=1.5) # Cap leverage at 150% + + # 3. Portfolio Returns + # Position = Signal * Weight + asset_returns = (signals.shift(1) * weights.shift(1) * returns).mean(axis=1) + + # 4. Interest on Cash (SOFR) + # If signals are 0 (in cash), we earn SOFR + cash_percentage = 1 - signals.mean(axis=1) + interest_returns = (cash_percentage.shift(1) * (sofr_series.shift(1) / 252)) + + total_returns = asset_returns + interest_returns + equity_curve = (1 + total_returns).fillna(0).cumprod() + + # 5. Metrics + ann_ret = total_returns.mean() * 252 + ann_vol = total_returns.std() * np.sqrt(252) + sharpe = (ann_ret - 0.035) / ann_vol if ann_vol > 0 else 0 + + dd = equity_curve / equity_curve.cummax() - 1 + max_dd = dd.min() + + return { + 'equity_curve': equity_curve, + 'sharpe': sharpe, + 'ann_ret': ann_ret, + 'max_dd': max_dd, + 'current_signals': signals.iloc[-1] + } diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index 33e492bf4ee58669b1dfc3023817f256203c92dd..6395d603107a5909740f065940e42e935ae3ed41 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -14,7 +14,7 @@ X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XL FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] def get_hf_token(): - """Safely retrieves the token without triggering a SecretNotFoundError crash.""" + """Safely retrieves the token from secrets or environment.""" try: return st.secrets["HF_TOKEN"] except: @@ -32,7 +32,7 @@ def load_from_hf(): return None def seed_dataset_from_scratch(): - """Downloads 2008-Present data from STOOQ.""" + """Initial download of 18 years of data using Stooq primarily.""" tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) master_df = pd.DataFrame() @@ -42,13 +42,13 @@ def seed_dataset_from_scratch(): for i, ticker in enumerate(tickers): status.text(f"Fetching {ticker} from Stooq...") try: - # Stooq primary + # Stooq primary (requires .US suffix for ETFs) data = web.DataReader(f"{ticker}.US", 'stooq', start='2008-01-01') if not data.empty: master_df[ticker] = data['Close'].sort_index() - time.sleep(0.6) # Anti-rate limit + time.sleep(0.6) except: - # YFinance fallback + # YFinance fallback if Stooq fails for a ticker try: yf_data = yf.download(ticker, start="2008-01-01", progress=False)['Adj Close'] master_df[ticker] = yf_data @@ -56,12 +56,12 @@ def seed_dataset_from_scratch(): pass progress_bar.progress((i + 1) / len(tickers)) - # Add SOFR Rate + # Add SOFR Rate (Cash interest) try: sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() master_df['SOFR_ANNUAL'] = sofr / 100 except: - master_df['SOFR_ANNUAL'] = 0.05 + master_df['SOFR_ANNUAL'] = 0.045 # Conservative proxy master_df = master_df.sort_index().ffill() master_df.to_csv(FILENAME) @@ -70,11 +70,10 @@ def seed_dataset_from_scratch(): return master_df def sync_incremental_data(df_existing): - """Updates only new data since last index date.""" + """Updates only new data since last index date using YFinance for speed.""" last_date = pd.to_datetime(df_existing.index).max() tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - # Simple incremental fetch new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] combined = pd.concat([df_existing, new_data]) combined = combined[~combined.index.duplicated(keep='last')].sort_index() diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 7df0d082ac77c3087335fa601e43a9ae21d4376b..b90e73874ec136083460d9ad203ac9edbd59b4fe 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,45 +1,68 @@ import streamlit as st -import pandas as pd -from data.loader import load_from_hf, seed_dataset_from_scratch, sync_incremental_data +from data.loader import load_from_hf, seed_dataset_from_scratch, sync_incremental_data, X_EQUITY_TICKERS, FI_TICKERS +from engine.trend_engine import run_trend_module -st.set_page_config(layout="wide", page_title="P2 Trend Suite") +st.set_page_config(layout="wide", page_title="P2 Strategy Suite") -# --- SIDEBAR: DATA MANAGEMENT --- -st.sidebar.title("đŸ—‚ī¸ Data Management") - -# Initialize Session State +# --- INITIALIZATION --- if 'master_data' not in st.session_state: st.session_state.master_data = load_from_hf() -# LOGIC: If no data, show SEED. If data exists, show SYNC. -if st.session_state.master_data is None: - st.sidebar.warning("Database not found.") - if st.sidebar.button("🚀 Step 1: Seed Database (2008-2026)"): - with st.spinner("Downloading full history..."): +# --- SIDEBAR: DATA CONTROLS --- +with st.sidebar: + st.header("đŸ—‚ī¸ Data Management") + if st.session_state.master_data is None: + st.error("No dataset detected.") + if st.button("🚀 Seed Database (2008-2026)", use_container_width=True): st.session_state.master_data = seed_dataset_from_scratch() - st.sidebar.success("Database Seeded!") st.rerun() -else: - st.sidebar.success(f"Database Active: {st.session_state.master_data.index.max()}") - - # SYNC BUTTON for daily incremental updates - if st.sidebar.button("🔄 Step 2: Sync Daily Data"): - with st.spinner("Pinging Stooq/FRED for new data..."): + else: + last_dt = st.session_state.master_data.index.max() + st.success(f"Database Active: {last_dt.date()}") + if st.button("🔄 Sync New Data", use_container_width=True): st.session_state.master_data = sync_incremental_data(st.session_state.master_data) - st.sidebar.success("Incremental Sync Complete!") st.rerun() + + st.divider() + st.header("âš™ī¸ Strategy Settings") + option = st.radio("Universe Selection", ("Option A - FI Trend", "Option B - Equity Trend")) + start_yr = st.slider("Backtest Start Year", 2008, 2026, 2015) + vol_target = st.slider("Target Vol (%)", 5, 20, 12) / 100 + + st.divider() + run_btn = st.button("🚀 Run Strategy Analysis", use_container_width=True, type="primary") -# --- SIDEBAR: STRATEGY CONTROLS --- -st.sidebar.divider() -st.sidebar.title("âš™ī¸ Strategy Settings") -option = st.sidebar.radio("Select Module", ("Option A - FI Trend", "Option B - Equity Trend")) -start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) -vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) - -# --- MAIN UI: ANALYSIS --- +# --- MAIN PAGE: DISPLAY --- if st.session_state.master_data is not None: - # Your strategy execution code here... - st.title(f"📊 {option}") - # ... + if run_btn: + with st.spinner("Crunching data..."): + # Universe Selection + univ = FI_TICKERS if "Option A" in option else X_EQUITY_TICKERS + # Slice by date + df = st.session_state.master_data[st.session_state.master_data.index.year >= start_yr] + + # Execute Engine + results = run_trend_module(df[univ], df['SOFR_ANNUAL'], vol_target) + + # Show Metrics + st.title(f"📊 {option} Performance Report") + m1, m2, m3 = st.columns(3) + m1.metric("Sharpe Ratio", f"{results['sharpe']:.2f}") + m2.metric("Annual Return", f"{results['ann_ret']:.1%}") + m3.metric("Max Drawdown", f"{results['max_dd']:.1%}") + + # Equity Curve + st.subheader("Cumulative Growth (vs Cash)") + st.line_chart(results['equity_curve']) + + # Allocation Check + st.divider() + st.subheader("Current Market Status") + active_assets = results['current_signals'][results['current_signals'] > 0].index.tolist() + st.write(f"**In-Trend Assets:** {', '.join(active_assets) if active_assets else 'All Cash'}") + + else: + st.title("Welcome to the 2025 Trend Suite") + st.info("👈 Use the sidebar to manage your data and click 'Run Strategy Analysis' to begin.") else: - st.info("Please use the sidebar to Seed the database first.") + st.warning("Please initialize the database using the 'Seed' button in the sidebar.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index aa01eac308bac7f4e8e4fa1250f3495bfd4829d0..33e492bf4ee58669b1dfc3023817f256203c92dd 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -9,10 +9,22 @@ import streamlit as st REPO_ID = "P2SAMAPA/etf_trend_data" FILENAME = "market_data.csv" -# Make sure these match exactly what app.py expects +# The 27 Equity X-ETFs and 15 FI ETFs from the 2025 Paper +X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] +FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] + +def get_hf_token(): + """Safely retrieves the token without triggering a SecretNotFoundError crash.""" + try: + return st.secrets["HF_TOKEN"] + except: + return os.getenv("HF_TOKEN") + def load_from_hf(): - token = st.secrets.get("HF_TOKEN") - if not token: return None + """Reads dataset from Hugging Face if it exists.""" + token = get_hf_token() + if not token: + return None try: path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) return pd.read_csv(path, index_col=0, parse_dates=True) @@ -20,10 +32,64 @@ def load_from_hf(): return None def seed_dataset_from_scratch(): - # ... (Your Stooq download logic here) - # Ensure this function name matches the import in app.py + """Downloads 2008-Present data from STOOQ.""" + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + master_df = pd.DataFrame() + + status = st.empty() + progress_bar = st.progress(0) + + for i, ticker in enumerate(tickers): + status.text(f"Fetching {ticker} from Stooq...") + try: + # Stooq primary + data = web.DataReader(f"{ticker}.US", 'stooq', start='2008-01-01') + if not data.empty: + master_df[ticker] = data['Close'].sort_index() + time.sleep(0.6) # Anti-rate limit + except: + # YFinance fallback + try: + yf_data = yf.download(ticker, start="2008-01-01", progress=False)['Adj Close'] + master_df[ticker] = yf_data + except: + pass + progress_bar.progress((i + 1) / len(tickers)) + + # Add SOFR Rate + try: + sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() + master_df['SOFR_ANNUAL'] = sofr / 100 + except: + master_df['SOFR_ANNUAL'] = 0.05 + + master_df = master_df.sort_index().ffill() + master_df.to_csv(FILENAME) + + upload_to_hf(FILENAME) return master_df def sync_incremental_data(df_existing): - # ... (Your incremental update logic here) + """Updates only new data since last index date.""" + last_date = pd.to_datetime(df_existing.index).max() + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + + # Simple incremental fetch + new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] + combined = pd.concat([df_existing, new_data]) + combined = combined[~combined.index.duplicated(keep='last')].sort_index() + + combined.to_csv(FILENAME) + upload_to_hf(FILENAME) return combined + +def upload_to_hf(path): + api = HfApi() + token = get_hf_token() + api.upload_file( + path_or_fileobj=path, + path_in_repo=FILENAME, + repo_id=REPO_ID, + repo_type="dataset", + token=token + ) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index ee4abe7cfb2071274d7276b5b2b0e81159f4d683..aa01eac308bac7f4e8e4fa1250f3495bfd4829d0 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -9,47 +9,21 @@ import streamlit as st REPO_ID = "P2SAMAPA/etf_trend_data" FILENAME = "market_data.csv" -def seed_dataset(): - tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - master_df = pd.DataFrame() - - st.info("đŸ›°ī¸ Initializing Stooq Data Fetch (2008-Present)...") - progress_bar = st.progress(0) - - for i, ticker in enumerate(tickers): - # Stooq ticker format is usually 'TICKER.US' - stooq_symbol = f"{ticker}.US" - try: - # PRIMARY: STOOQ - data = web.DataReader(stooq_symbol, 'stooq', start='2008-01-01') - if not data.empty: - # Stooq returns data in reverse chronological order; we sort it. - master_df[ticker] = data['Close'].sort_index() - - # Anti-Rate Limit: 0.8s delay between requests - time.sleep(0.8) - - except Exception as e: - st.warning(f"âš ī¸ Stooq failed for {ticker}. Attempting YFinance fallback...") - try: - # BACKUP: YFinance - yf_data = yf.download(ticker, start="2008-01-01", progress=False)['Adj Close'] - master_df[ticker] = yf_data - except: - st.error(f"❌ Failed to fetch {ticker} from all sources.") - - progress_bar.progress((i + 1) / len(tickers)) - - # Add SOFR (Cash Rate) from FRED +# Make sure these match exactly what app.py expects +def load_from_hf(): + token = st.secrets.get("HF_TOKEN") + if not token: return None try: - sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() - master_df['SOFR_ANNUAL'] = sofr / 100 + path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) + return pd.read_csv(path, index_col=0, parse_dates=True) except: - master_df['SOFR_ANNUAL'] = 0.05 # Conservative fallback + return None - master_df = master_df.sort_index().ffill() - - # Save & Upload - master_df.to_csv(FILENAME) - upload_to_hf(FILENAME) +def seed_dataset_from_scratch(): + # ... (Your Stooq download logic here) + # Ensure this function name matches the import in app.py return master_df + +def sync_incremental_data(df_existing): + # ... (Your incremental update logic here) + return combined diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index 645d71ed1f473b7c4266a646f01864bd9ca3eb16..ee4abe7cfb2071274d7276b5b2b0e81159f4d683 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,26 +1,55 @@ -def sync_incremental_data(df_existing): - """Downloads only missing data since last update and saves to HF.""" - import yfinance as yf - - # Identify last date in the CSV - last_date = pd.to_datetime(df_existing.index).max() +import pandas as pd +import pandas_datareader.data as web +import yfinance as yf +import time +from huggingface_hub import hf_hub_download, HfApi +import os +import streamlit as st + +REPO_ID = "P2SAMAPA/etf_trend_data" +FILENAME = "market_data.csv" + +def seed_dataset(): tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + master_df = pd.DataFrame() + + st.info("đŸ›°ī¸ Initializing Stooq Data Fetch (2008-Present)...") + progress_bar = st.progress(0) - # Fetch new bars from yfinance or stooq - new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] + for i, ticker in enumerate(tickers): + # Stooq ticker format is usually 'TICKER.US' + stooq_symbol = f"{ticker}.US" + try: + # PRIMARY: STOOQ + data = web.DataReader(stooq_symbol, 'stooq', start='2008-01-01') + if not data.empty: + # Stooq returns data in reverse chronological order; we sort it. + master_df[ticker] = data['Close'].sort_index() + + # Anti-Rate Limit: 0.8s delay between requests + time.sleep(0.8) + + except Exception as e: + st.warning(f"âš ī¸ Stooq failed for {ticker}. Attempting YFinance fallback...") + try: + # BACKUP: YFinance + yf_data = yf.download(ticker, start="2008-01-01", progress=False)['Adj Close'] + master_df[ticker] = yf_data + except: + st.error(f"❌ Failed to fetch {ticker} from all sources.") + + progress_bar.progress((i + 1) / len(tickers)) - # Combine (Drop duplicates to avoid double-counting the last day) - combined = pd.concat([df_existing, new_data]) - combined = combined[~combined.index.duplicated(keep='last')].sort_index() + # Add SOFR (Cash Rate) from FRED + try: + sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() + master_df['SOFR_ANNUAL'] = sofr / 100 + except: + master_df['SOFR_ANNUAL'] = 0.05 # Conservative fallback + + master_df = master_df.sort_index().ffill() - # Save & Push - combined.to_csv(FILENAME) - api = HfApi() - api.upload_file( - path_or_fileobj=FILENAME, - path_in_repo=FILENAME, - repo_id=REPO_ID, - repo_type="dataset", - token=st.secrets["HF_TOKEN"] - ) - return combined + # Save & Upload + master_df.to_csv(FILENAME) + upload_to_hf(FILENAME) + return master_df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index a20a1cb412cbec493abd5517e7ae2ee1b3606ef2..645d71ed1f473b7c4266a646f01864bd9ca3eb16 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,38 +1,26 @@ -import pandas as pd -import yfinance as yf -import pandas_datareader.data as web -from huggingface_hub import hf_hub_download, HfApi -import os -import streamlit as st - -REPO_ID = "P2SAMAPA/etf_trend_data" -FILENAME = "market_data.csv" - -X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] -FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] - -def load_from_hf(): - try: - token = st.secrets["HF_TOKEN"] - path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) - return pd.read_csv(path, index_col=0, parse_dates=True) - except: - return None - -def seed_dataset(): +def sync_incremental_data(df_existing): + """Downloads only missing data since last update and saves to HF.""" + import yfinance as yf + + # Identify last date in the CSV + last_date = pd.to_datetime(df_existing.index).max() tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - # Download Wide Format - df = yf.download(tickers, start="2008-01-01")['Adj Close'] - # Add SOFR - sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() - df['SOFR_ANNUAL'] = sofr / 100 - df = df.sort_index().ffill() + # Fetch new bars from yfinance or stooq + new_data = yf.download(tickers, start=last_date, progress=False)['Adj Close'] + + # Combine (Drop duplicates to avoid double-counting the last day) + combined = pd.concat([df_existing, new_data]) + combined = combined[~combined.index.duplicated(keep='last')].sort_index() - df.to_csv(FILENAME) - upload_to_hf(FILENAME) - return df - -def upload_to_hf(path): + # Save & Push + combined.to_csv(FILENAME) api = HfApi() - api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=st.secrets["HF_TOKEN"]) + api.upload_file( + path_or_fileobj=FILENAME, + path_in_repo=FILENAME, + repo_id=REPO_ID, + repo_type="dataset", + token=st.secrets["HF_TOKEN"] + ) + return combined diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 4925585a822ea2ff474da864a26cf47c57f3207a..7df0d082ac77c3087335fa601e43a9ae21d4376b 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,35 +1,45 @@ import streamlit as st import pandas as pd -import pandas_market_calendars as mcal -from datetime import datetime -from data.loader import load_from_hf, seed_dataset, X_EQUITY_TICKERS, FI_TICKERS -from engine.trend_engine import run_trend_module +from data.loader import load_from_hf, seed_dataset_from_scratch, sync_incremental_data st.set_page_config(layout="wide", page_title="P2 Trend Suite") -# Sidebar Logic -st.sidebar.title("Configuration") -option = st.sidebar.radio("Select Strategy", ("Option A - FI Trend Follower", "Option B - Equity Trend Follower")) -start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) -vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.12) +# --- SIDEBAR: DATA MANAGEMENT --- +st.sidebar.title("đŸ—‚ī¸ Data Management") -# Data Initialization -if 'data' not in st.session_state: - st.session_state.data = load_from_hf() +# Initialize Session State +if 'master_data' not in st.session_state: + st.session_state.master_data = load_from_hf() -if st.session_state.data is None: - if st.button("🚀 First Time Setup: Seed 2008-2026 Data"): - st.session_state.data = seed_dataset() - st.rerun() +# LOGIC: If no data, show SEED. If data exists, show SYNC. +if st.session_state.master_data is None: + st.sidebar.warning("Database not found.") + if st.sidebar.button("🚀 Step 1: Seed Database (2008-2026)"): + with st.spinner("Downloading full history..."): + st.session_state.master_data = seed_dataset_from_scratch() + st.sidebar.success("Database Seeded!") + st.rerun() else: - # RUN STRATEGY - universe = FI_TICKERS if "Option A" in option else X_EQUITY_TICKERS - bench = "AGG" if "Option A" in option else "SPY" - - # Filter by Year - d = st.session_state.data[st.session_state.data.index.year >= start_year] - results = run_trend_module(d[universe], d['SOFR_ANNUAL'], vol_target) + st.sidebar.success(f"Database Active: {st.session_state.master_data.index.max()}") - # UI OUTPUTS (Sharpe, Max DD, etc.) - st.title(f"📈 {option} Performance") - # ... (Insert Metric & Chart code here) + # SYNC BUTTON for daily incremental updates + if st.sidebar.button("🔄 Step 2: Sync Daily Data"): + with st.spinner("Pinging Stooq/FRED for new data..."): + st.session_state.master_data = sync_incremental_data(st.session_state.master_data) + st.sidebar.success("Incremental Sync Complete!") + st.rerun() + +# --- SIDEBAR: STRATEGY CONTROLS --- +st.sidebar.divider() +st.sidebar.title("âš™ī¸ Strategy Settings") +option = st.sidebar.radio("Select Module", ("Option A - FI Trend", "Option B - Equity Trend")) +start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) +vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) + +# --- MAIN UI: ANALYSIS --- +if st.session_state.master_data is not None: + # Your strategy execution code here... + st.title(f"📊 {option}") + # ... +else: + st.info("Please use the sidebar to Seed the database first.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index ebaf87099ff871f8101b342b823cce879a0404a4..a20a1cb412cbec493abd5517e7ae2ee1b3606ef2 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,6 +1,6 @@ import pandas as pd -import pandas_datareader.data as web import yfinance as yf +import pandas_datareader.data as web from huggingface_hub import hf_hub_download, HfApi import os import streamlit as st @@ -12,49 +12,27 @@ X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XL FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] def load_from_hf(): - """Reads the dataset from Hugging Face.""" try: - # Note: Use st.secrets if token is not in env - token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") + token = st.secrets["HF_TOKEN"] path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) return pd.read_csv(path, index_col=0, parse_dates=True) - except Exception as e: - print(f"Dataset load failed: {e}") + except: return None -def seed_dataset_from_scratch(): - """Download full history from 2008 and upload to HF.""" +def seed_dataset(): tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) - master_df = pd.DataFrame() - - status = st.empty() - progress_bar = st.progress(0) - - for i, t in enumerate(tickers): - status.text(f"Seeding {t}...") - try: - # Fetching from 2008 for initial dataset - data = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] - master_df[t] = data - except: - continue - progress_bar.progress((i + 1) / len(tickers)) + # Download Wide Format + df = yf.download(tickers, start="2008-01-01")['Adj Close'] # Add SOFR sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() - master_df['SOFR_ANNUAL'] = sofr / 100 - master_df = master_df.sort_index().ffill() - - master_df.to_csv(FILENAME) + df['SOFR_ANNUAL'] = sofr / 100 + df = df.sort_index().ffill() - # Upload + df.to_csv(FILENAME) + upload_to_hf(FILENAME) + return df + +def upload_to_hf(path): api = HfApi() - token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") - api.upload_file( - path_or_fileobj=FILENAME, - path_in_repo=FILENAME, - repo_id=REPO_ID, - repo_type="dataset", - token=token - ) - return master_df + api.upload_file(path_or_fileobj=path, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", token=st.secrets["HF_TOKEN"]) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 73d05e92a63d16a80d3ec2d83b293e3d909fca7d..4925585a822ea2ff474da864a26cf47c57f3207a 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,23 +1,35 @@ import streamlit as st -from data.loader import load_from_hf, seed_dataset_from_scratch, X_EQUITY_TICKERS, FI_TICKERS -# ... other imports +import pandas as pd +import pandas_market_calendars as mcal +from datetime import datetime +from data.loader import load_from_hf, seed_dataset, X_EQUITY_TICKERS, FI_TICKERS +from engine.trend_engine import run_trend_module -st.sidebar.title("Data Management") +st.set_page_config(layout="wide", page_title="P2 Trend Suite") -# Check if data exists -if 'master_data' not in st.session_state: - st.session_state.master_data = load_from_hf() +# Sidebar Logic +st.sidebar.title("Configuration") +option = st.sidebar.radio("Select Strategy", ("Option A - FI Trend Follower", "Option B - Equity Trend Follower")) +start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) +vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.12) -if st.session_state.master_data is None: - st.warning("Dataset not found on Hugging Face. Please Seed the Database.") - if st.sidebar.button("🚀 Step 1: Seed Database (2008-Present)"): - with st.spinner("Downloading 18 years of data... this takes a few minutes."): - st.session_state.master_data = seed_dataset_from_scratch() - st.success("Database seeded and uploaded to HF!") -else: - if st.sidebar.button("🔄 Step 2: Daily Incremental Sync"): - # (Existing incremental sync logic here) - st.sidebar.write("Last Data Point:", st.session_state.master_data.index.max()) +# Data Initialization +if 'data' not in st.session_state: + st.session_state.data = load_from_hf() -# --- REST OF THE UI --- -# Run Option A/B logic using st.session_state.master_data +if st.session_state.data is None: + if st.button("🚀 First Time Setup: Seed 2008-2026 Data"): + st.session_state.data = seed_dataset() + st.rerun() +else: + # RUN STRATEGY + universe = FI_TICKERS if "Option A" in option else X_EQUITY_TICKERS + bench = "AGG" if "Option A" in option else "SPY" + + # Filter by Year + d = st.session_state.data[st.session_state.data.index.year >= start_year] + results = run_trend_module(d[universe], d['SOFR_ANNUAL'], vol_target) + + # UI OUTPUTS (Sharpe, Max DD, etc.) + st.title(f"📈 {option} Performance") + # ... (Insert Metric & Chart code here) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index dea08874c93f19d4b81c631e3195859a17d068e7..ebaf87099ff871f8101b342b823cce879a0404a4 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -8,51 +8,53 @@ import streamlit as st REPO_ID = "P2SAMAPA/etf_trend_data" FILENAME = "market_data.csv" -# Universes X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] +def load_from_hf(): + """Reads the dataset from Hugging Face.""" + try: + # Note: Use st.secrets if token is not in env + token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") + path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=token) + return pd.read_csv(path, index_col=0, parse_dates=True) + except Exception as e: + print(f"Dataset load failed: {e}") + return None + def seed_dataset_from_scratch(): - """Download full history from 2008 for all 42+ tickers and upload to HF.""" + """Download full history from 2008 and upload to HF.""" tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) master_df = pd.DataFrame() + status = st.empty() progress_bar = st.progress(0) + for i, t in enumerate(tickers): + status.text(f"Seeding {t}...") try: - # We use yfinance for the heavy initial lift as it handles long historical ranges reliably + # Fetching from 2008 for initial dataset data = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] master_df[t] = data - except Exception as e: - st.warning(f"Failed to fetch {t}: {e}") + except: + continue progress_bar.progress((i + 1) / len(tickers)) - # Add SOFR (Cash Interest) + # Add SOFR sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() master_df['SOFR_ANNUAL'] = sofr / 100 + master_df = master_df.sort_index().ffill() - master_df = master_df.sort_index().ffill().dropna(how='all') - - # Save and Upload master_df.to_csv(FILENAME) - upload_to_hf(FILENAME) - return master_df - -def upload_to_hf(local_path): - """Pushes the local CSV to your Hugging Face Dataset repo.""" + + # Upload api = HfApi() + token = st.secrets.get("HF_TOKEN") or os.getenv("HF_TOKEN") api.upload_file( - path_or_fileobj=local_path, + path_or_fileobj=FILENAME, path_in_repo=FILENAME, repo_id=REPO_ID, repo_type="dataset", - token=st.secrets["HF_TOKEN"] + token=token ) - -def load_from_hf(): - """Reads the dataset from Hugging Face.""" - try: - path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=st.secrets["HF_TOKEN"]) - return pd.read_csv(path, index_col=0, parse_dates=True) - except: - return None + return master_df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 8db20e432d7396da7548ca0c1189a71995ca7e81..73d05e92a63d16a80d3ec2d83b293e3d909fca7d 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,99 +1,23 @@ import streamlit as st -import pandas as pd -import numpy as np -import pandas_market_calendars as mcal -from datetime import datetime -from data.loader import refresh_market_data, X_EQUITY_TICKERS, FI_TICKERS -from engine.trend_engine import run_trend_module - -st.set_page_config(layout="wide", page_title="P2 Trend Suite") - -# --- SIDEBAR UI --- -st.sidebar.title("Strategy Controls") - -# 1. Module Toggle -option = st.sidebar.radio("Select Module", - ("Option A - FI Trend Follower", "Option B - Equity Trend Follower")) - -# 2. Year Slider -start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) - -# 3. Parameters -vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) - -if st.sidebar.button("🔄 Sync Market Data"): - with st.spinner("Fetching Data..."): - refresh_market_data() - st.sidebar.success("Data Synced!") - -# --- DATA PROCESSING --- -try: - data = pd.read_csv("market_data.csv", index_col=0, parse_dates=True) - - # Filter by Year - data = data[data.index.year >= start_year] - - # Assign Universe & Benchmark - if "Option B" in option: - universe = X_EQUITY_TICKERS - benchmark_ticker = "SPY" - else: - universe = FI_TICKERS - benchmark_ticker = "AGG" - - # Run Analysis - results = run_trend_module(data[universe], data['SOFR_ANNUAL'], vol_target) - - # --- CALCULATE METRICS --- - curve = results['curve'] - rets = results['returns'] - - # Sharpe (Excess over 0) - sharpe = (rets.mean() * 252) / (rets.std() * np.sqrt(252)) - - # Annualized Return - total_days = (curve.index[-1] - curve.index[0]).days - ann_return = (curve.iloc[-1]**(365/total_days) - 1) - - # Drawdowns - rolling_max = curve.cummax() - drawdown = (curve - rolling_max) / rolling_max - max_dd_peak = drawdown.min() - max_dd_daily = rets.min() - - # NYSE Calendar for Next Day - nyse = mcal.get_calendar('NYSE') - schedule = nyse.schedule(start_date=datetime.now(), end_date='2026-12-31') - next_day = schedule.index[0].strftime('%Y-%m-%d') - - # --- OUTPUT UI --- - st.title(f"📊 {option}") - - # Stats Row - c1, c2, c3, c4, c5 = st.columns(5) - c1.metric("Sharpe Ratio", f"{sharpe:.2f}") - c2.metric("Annual Return", f"{ann_return:.2%}") - c3.metric("Max DD (P-to-T)", f"{max_dd_peak:.2%}") - c4.metric("Max DD (Daily)", f"{max_dd_daily:.2%}") - c5.metric("Next Trade Date", next_day) - - # Allocation Table - st.subheader(f"📍 Target Allocation for {next_day}") - alloc = results['alloc'] - st.dataframe(alloc[alloc['Weight (%)'] > 0].sort_values("Weight (%)", ascending=False), use_container_width=True) - - # Performance Chart - st.subheader(f"Cumulative Return vs {benchmark_ticker}") - bench_curve = (1 + data[benchmark_ticker].pct_change().fillna(0)).cumprod() - # Normalize benchmark to start at 1.0 at start_year - bench_curve = bench_curve / bench_curve.iloc[0] - - chart_df = pd.DataFrame({ - "Strategy": curve, - f"Benchmark ({benchmark_ticker})": bench_curve - }) - st.line_chart(chart_df) - -except Exception as e: - st.info("Please Click 'Sync Market Data' in the sidebar to initialize the engine.") - st.error(f"Waiting for data... (Technical details: {e})") +from data.loader import load_from_hf, seed_dataset_from_scratch, X_EQUITY_TICKERS, FI_TICKERS +# ... other imports + +st.sidebar.title("Data Management") + +# Check if data exists +if 'master_data' not in st.session_state: + st.session_state.master_data = load_from_hf() + +if st.session_state.master_data is None: + st.warning("Dataset not found on Hugging Face. Please Seed the Database.") + if st.sidebar.button("🚀 Step 1: Seed Database (2008-Present)"): + with st.spinner("Downloading 18 years of data... this takes a few minutes."): + st.session_state.master_data = seed_dataset_from_scratch() + st.success("Database seeded and uploaded to HF!") +else: + if st.sidebar.button("🔄 Step 2: Daily Incremental Sync"): + # (Existing incremental sync logic here) + st.sidebar.write("Last Data Point:", st.session_state.master_data.index.max()) + +# --- REST OF THE UI --- +# Run Option A/B logic using st.session_state.master_data diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index fdbf82ba4101d3e404ed0d1733aa4022f49ebf01..dea08874c93f19d4b81c631e3195859a17d068e7 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,32 +1,58 @@ +import pandas as pd import pandas_datareader.data as web import yfinance as yf -import pandas as pd +from huggingface_hub import hf_hub_download, HfApi +import os import streamlit as st -# 27 "X-" EQUITY ETFS -X_EQUITY_TICKERS = [ - "XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", - "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", - "XSW", "XTN", "XTL", "XNTK", "XITK" -] +REPO_ID = "P2SAMAPA/etf_trend_data" +FILENAME = "market_data.csv" -# 15 FIXED INCOME / COMPARISON +# Universes +X_EQUITY_TICKERS = ["XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", "XSW", "XTN", "XTL", "XNTK", "XITK"] FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] -def refresh_market_data(): - """Syncs Stooq/FRED data to local CSV and HF.""" - all_prices = {} - # Download all groups + SPY Benchmark - for t in list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY"])): +def seed_dataset_from_scratch(): + """Download full history from 2008 for all 42+ tickers and upload to HF.""" + tickers = list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY", "AGG"])) + master_df = pd.DataFrame() + + progress_bar = st.progress(0) + for i, t in enumerate(tickers): try: - all_prices[t] = web.DataReader(f"{t}.US", "stooq")['Close'] - except: - all_prices[t] = yf.download(t, progress=False)['Adj Close'] - - # Fetch SOFR (Cash Yield) from FRED - sofr = web.DataReader('SOFR', 'fred').ffill() + # We use yfinance for the heavy initial lift as it handles long historical ranges reliably + data = yf.download(t, start="2008-01-01", progress=False)['Adj Close'] + master_df[t] = data + except Exception as e: + st.warning(f"Failed to fetch {t}: {e}") + progress_bar.progress((i + 1) / len(tickers)) + + # Add SOFR (Cash Interest) + sofr = web.DataReader('SOFR', 'fred', start="2008-01-01").ffill() + master_df['SOFR_ANNUAL'] = sofr / 100 - df = pd.DataFrame(all_prices).sort_index().ffill() - df['SOFR_ANNUAL'] = sofr / 100 - df.to_csv("market_data.csv") - return df + master_df = master_df.sort_index().ffill().dropna(how='all') + + # Save and Upload + master_df.to_csv(FILENAME) + upload_to_hf(FILENAME) + return master_df + +def upload_to_hf(local_path): + """Pushes the local CSV to your Hugging Face Dataset repo.""" + api = HfApi() + api.upload_file( + path_or_fileobj=local_path, + path_in_repo=FILENAME, + repo_id=REPO_ID, + repo_type="dataset", + token=st.secrets["HF_TOKEN"] + ) + +def load_from_hf(): + """Reads the dataset from Hugging Face.""" + try: + path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME, repo_type="dataset", token=st.secrets["HF_TOKEN"]) + return pd.read_csv(path, index_col=0, parse_dates=True) + except: + return None diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 576bbca619cc3d82d8c06feacd010ccf7ab85fda..8db20e432d7396da7548ca0c1189a71995ca7e81 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -11,78 +11,89 @@ st.set_page_config(layout="wide", page_title="P2 Trend Suite") # --- SIDEBAR UI --- st.sidebar.title("Strategy Controls") -# 1. Option Selection -option = st.sidebar.radio("Select Strategy Module", +# 1. Module Toggle +option = st.sidebar.radio("Select Module", ("Option A - FI Trend Follower", "Option B - Equity Trend Follower")) # 2. Year Slider -start_year = st.sidebar.slider("Start Year (OOS Period)", 2008, 2025, 2015) +start_year = st.sidebar.slider("Start Year", 2008, 2026, 2015) -# 3. Vol Target & Sync +# 3. Parameters vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) + if st.sidebar.button("🔄 Sync Market Data"): - refresh_market_data() + with st.spinner("Fetching Data..."): + refresh_market_data() st.sidebar.success("Data Synced!") -# --- CALENDAR LOGIC --- -nyse = mcal.get_calendar('NYSE') -today = datetime.now().strftime('%Y-%m-%d') -schedule = nyse.schedule(start_date=today, end_date='2026-12-31') -next_trading_day = schedule.index[0].strftime('%A, %b %d, %Y') - -# --- EXECUTION --- -if st.button("â–ļ Run Analysis"): +# --- DATA PROCESSING --- +try: data = pd.read_csv("market_data.csv", index_col=0, parse_dates=True) - # Filter by Start Year + # Filter by Year data = data[data.index.year >= start_year] - - # Select Universe & Benchmark + + # Assign Universe & Benchmark if "Option B" in option: universe = X_EQUITY_TICKERS benchmark_ticker = "SPY" - module_name = "Equity" else: universe = FI_TICKERS benchmark_ticker = "AGG" - module_name = "Fixed Income" - # Run Engine + # Run Analysis results = run_trend_module(data[universe], data['SOFR_ANNUAL'], vol_target) - # Metrics Calculation - returns = results['returns'] - cum_returns = results['curve'] - bench_returns = data[benchmark_ticker].pct_change().fillna(0) - bench_curve = (1 + bench_returns).cumprod() + # --- CALCULATE METRICS --- + curve = results['curve'] + rets = results['returns'] - # Stats - ann_return = (cum_returns.iloc[-1]**(252/len(returns)) - 1) - sharpe = (returns.mean() * 252) / (returns.std() * np.sqrt(252)) + # Sharpe (Excess over 0) + sharpe = (rets.mean() * 252) / (rets.std() * np.sqrt(252)) - rolling_max = cum_returns.cummax() - drawdown = (cum_returns - rolling_max) / rolling_max - max_dd_peak = drawdown.min() + # Annualized Return + total_days = (curve.index[-1] - curve.index[0]).days + ann_return = (curve.iloc[-1]**(365/total_days) - 1) + # Drawdowns + rolling_max = curve.cummax() + drawdown = (curve - rolling_max) / rolling_max + max_dd_peak = drawdown.min() + max_dd_daily = rets.min() + + # NYSE Calendar for Next Day + nyse = mcal.get_calendar('NYSE') + schedule = nyse.schedule(start_date=datetime.now(), end_date='2026-12-31') + next_day = schedule.index[0].strftime('%Y-%m-%d') + # --- OUTPUT UI --- - st.header(f"📊 {option} Results") + st.title(f"📊 {option}") - # Target Allocation Section - st.subheader(f"📅 Next Day Target Allocation: {next_trading_day}") - alloc_df = results['alloc'] - st.table(alloc_df[alloc_df['Weight (%)'] > 0].sort_values("Weight (%)", ascending=False)) + # Stats Row + c1, c2, c3, c4, c5 = st.columns(5) + c1.metric("Sharpe Ratio", f"{sharpe:.2f}") + c2.metric("Annual Return", f"{ann_return:.2%}") + c3.metric("Max DD (P-to-T)", f"{max_dd_peak:.2%}") + c4.metric("Max DD (Daily)", f"{max_dd_daily:.2%}") + c5.metric("Next Trade Date", next_day) - # Metrics Row - m1, m2, m3, m4 = st.columns(4) - m1.metric("Annualized Return", f"{ann_return:.2%}") - m2.metric("Sharpe Ratio", f"{sharpe:.2f}") - m3.metric("Max DD (Peak-to-Trough)", f"{max_dd_peak:.2%}") - m4.metric("Last Daily Return", f"{returns.iloc[-1]:.2%}") + # Allocation Table + st.subheader(f"📍 Target Allocation for {next_day}") + alloc = results['alloc'] + st.dataframe(alloc[alloc['Weight (%)'] > 0].sort_values("Weight (%)", ascending=False), use_container_width=True) - # Chart + # Performance Chart st.subheader(f"Cumulative Return vs {benchmark_ticker}") - chart_data = pd.DataFrame({ - "Strategy": cum_returns, + bench_curve = (1 + data[benchmark_ticker].pct_change().fillna(0)).cumprod() + # Normalize benchmark to start at 1.0 at start_year + bench_curve = bench_curve / bench_curve.iloc[0] + + chart_df = pd.DataFrame({ + "Strategy": curve, f"Benchmark ({benchmark_ticker})": bench_curve }) - st.line_chart(chart_data) + st.line_chart(chart_df) + +except Exception as e: + st.info("Please Click 'Sync Market Data' in the sidebar to initialize the engine.") + st.error(f"Waiting for data... (Technical details: {e})") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 442585e5c8b7f93d5ed33ec2f0ba1e0da4ee70d2..576bbca619cc3d82d8c06feacd010ccf7ab85fda 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,39 +1,88 @@ import streamlit as st import pandas as pd +import numpy as np +import pandas_market_calendars as mcal +from datetime import datetime from data.loader import refresh_market_data, X_EQUITY_TICKERS, FI_TICKERS from engine.trend_engine import run_trend_module -st.set_page_config(layout="wide", page_title="P2 ETF Trend Suite") +st.set_page_config(layout="wide", page_title="P2 Trend Suite") -st.sidebar.title("Settings") -vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) +# --- SIDEBAR UI --- +st.sidebar.title("Strategy Controls") + +# 1. Option Selection +option = st.sidebar.radio("Select Strategy Module", + ("Option A - FI Trend Follower", "Option B - Equity Trend Follower")) + +# 2. Year Slider +start_year = st.sidebar.slider("Start Year (OOS Period)", 2008, 2025, 2015) -if st.sidebar.button("🔄 Refresh Market Data"): +# 3. Vol Target & Sync +vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) +if st.sidebar.button("🔄 Sync Market Data"): refresh_market_data() - st.sidebar.success("Data Updated from Stooq/SOFR!") + st.sidebar.success("Data Synced!") + +# --- CALENDAR LOGIC --- +nyse = mcal.get_calendar('NYSE') +today = datetime.now().strftime('%Y-%m-%d') +schedule = nyse.schedule(start_date=today, end_date='2026-12-31') +next_trading_day = schedule.index[0].strftime('%A, %b %d, %Y') -if st.button("â–ļ Run All Modules"): +# --- EXECUTION --- +if st.button("â–ļ Run Analysis"): data = pd.read_csv("market_data.csv", index_col=0, parse_dates=True) - # Run Modules - eq_res = run_trend_module(data[X_EQUITY_TICKERS], data['SOFR_ANNUAL'], vol_target) - fi_res = run_trend_module(data[FI_TICKERS], data['SOFR_ANNUAL'], vol_target) + # Filter by Start Year + data = data[data.index.year >= start_year] + + # Select Universe & Benchmark + if "Option B" in option: + universe = X_EQUITY_TICKERS + benchmark_ticker = "SPY" + module_name = "Equity" + else: + universe = FI_TICKERS + benchmark_ticker = "AGG" + module_name = "Fixed Income" + + # Run Engine + results = run_trend_module(data[universe], data['SOFR_ANNUAL'], vol_target) + + # Metrics Calculation + returns = results['returns'] + cum_returns = results['curve'] + bench_returns = data[benchmark_ticker].pct_change().fillna(0) + bench_curve = (1 + bench_returns).cumprod() + + # Stats + ann_return = (cum_returns.iloc[-1]**(252/len(returns)) - 1) + sharpe = (returns.mean() * 252) / (returns.std() * np.sqrt(252)) - # Performance Comparison - spy_curve = (1 + data['SPY'].pct_change()).cumprod() - comparison = pd.DataFrame({ - "X-ETF Strategy": eq_res['curve'], - "SPY Benchmark": spy_curve - }).dropna() - - st.header("📈 Performance: Equity Strategy vs. SPY") - st.line_chart(comparison) + rolling_max = cum_returns.cummax() + drawdown = (cum_returns - rolling_max) / rolling_max + max_dd_peak = drawdown.min() - # Target Allocations - col1, col2 = st.columns(2) - with col1: - st.subheader("đŸ›Ąī¸ Equity Allocation (Next Day)") - st.dataframe(eq_res['alloc'][eq_res['alloc']['Weight (%)'] > 0]) - with col2: - st.subheader("đŸĻ FI Comparison Allocation") - st.dataframe(fi_res['alloc'][fi_res['alloc']['Weight (%)'] > 0]) + # --- OUTPUT UI --- + st.header(f"📊 {option} Results") + + # Target Allocation Section + st.subheader(f"📅 Next Day Target Allocation: {next_trading_day}") + alloc_df = results['alloc'] + st.table(alloc_df[alloc_df['Weight (%)'] > 0].sort_values("Weight (%)", ascending=False)) + + # Metrics Row + m1, m2, m3, m4 = st.columns(4) + m1.metric("Annualized Return", f"{ann_return:.2%}") + m2.metric("Sharpe Ratio", f"{sharpe:.2f}") + m3.metric("Max DD (Peak-to-Trough)", f"{max_dd_peak:.2%}") + m4.metric("Last Daily Return", f"{returns.iloc[-1]:.2%}") + + # Chart + st.subheader(f"Cumulative Return vs {benchmark_ticker}") + chart_data = pd.DataFrame({ + "Strategy": cum_returns, + f"Benchmark ({benchmark_ticker})": bench_curve + }) + st.line_chart(chart_data) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..3dccd952718ee84e244b96c41ea568c2f78e73da --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/trend_engine.py @@ -0,0 +1,40 @@ +import numpy as np +import pandas as pd + +def run_trend_module(prices, daily_sofr, vol_target): + # 1. Dual-Trend Signal + d_high = prices.rolling(20).max() + sma = prices.rolling(20).mean() + atr = (prices.rolling(20).max() - prices.rolling(20).min()) / 2 + k_upper = sma + (2 * atr) + + entry_band = np.minimum(d_high, k_upper) + signals = (prices > entry_band.shift(1)).astype(int) + + # 2. Risk Parity Position Sizing + returns = prices.pct_change() + realized_vol = returns.rolling(21).std() * np.sqrt(252) + + n = len(prices.columns) + # Target weight = (Target Vol / Total Assets) / Individual Asset Vol + target_weights = (vol_target / n) / realized_vol.shift(1) + + # 3. Strategy Returns (Positions + SOFR on Cash) + pos_rets = (signals.shift(1) * target_weights.shift(1) * returns).sum(axis=1) + weight_used = (signals.shift(1) * target_weights.shift(1)).sum(axis=1) + cash_rets = (1 - weight_used).clip(0, 1) * (daily_sofr / 252) + + strat_rets = pos_rets + cash_rets + equity_curve = (1 + strat_rets).fillna(0).cumprod() + + # 4. Target Allocation for Tomorrow + tomorrow_sig = (prices.iloc[-1] > entry_band.iloc[-1]).astype(int) + tomorrow_w = (vol_target / n) / realized_vol.iloc[-1] + + alloc = pd.DataFrame({ + "Ticker": prices.columns, + "Signal": ["LONG" if s == 1 else "CASH" for s in tomorrow_sig], + "Weight (%)": (tomorrow_sig * tomorrow_w * 100).round(2) + }) + + return {"curve": equity_curve, "alloc": alloc} diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/trend_engine.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/trend_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..4f91f0d59d3b04af8afbd0e377304edc931041f3 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/trend_engine.py @@ -0,0 +1,36 @@ +import numpy as np +import pandas as pd + +def run_trend_module(prices, daily_sofr, vol_target): + # 1. Signals (20-day Keltner/Donchian) + d_high = prices.rolling(20).max() + k_sma = prices.rolling(20).mean() + atr = (prices.rolling(20).max() - prices.rolling(20).min()) / 2 + k_upper = k_sma + (2 * atr) + + entry_band = np.minimum(d_high, k_upper) + signals = (prices > entry_band.shift(1)).astype(int) + + # 2. Risk Parity Weighting + rets = prices.pct_change() + real_vol = rets.rolling(21).std() * np.sqrt(252) + + n = len(prices.columns) + weights = (vol_target / n) / real_vol.shift(1) + + # 3. Strategy Returns (Positions + Cash Interest) + strat_rets = (signals.shift(1) * weights.shift(1) * rets).sum(axis=1) + unused_cap = 1 - (signals.shift(1) * weights.shift(1)).sum(axis=1) + strat_rets += unused_cap.clip(0, 1) * (daily_sofr / 252) + + equity_curve = (1 + strat_rets).cumprod() + + # Next Day Allocation + tomorrow_sig = (prices.iloc[-1] > entry_band.iloc[-1]).astype(int) + tomorrow_w = (vol_target / n) / real_vol.iloc[-1] + alloc = pd.DataFrame({ + "Ticker": prices.columns, + "Weight (%)": (tomorrow_sig * tomorrow_w * 100).round(2) + }) + + return {"curve": equity_curve, "alloc": alloc} diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index bece99ec6fd21a872c830a3bb47c0bf22f7ce199..442585e5c8b7f93d5ed33ec2f0ba1e0da4ee70d2 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,54 +1,39 @@ import streamlit as st +import pandas as pd +from data.loader import refresh_market_data, X_EQUITY_TICKERS, FI_TICKERS +from engine.trend_engine import run_trend_module -st.set_page_config(page_title="P2 ETF Trend Suite", layout="wide") +st.set_page_config(layout="wide", page_title="P2 ETF Trend Suite") -st.title("📊 P2 ETF Trend Suite") -st.markdown("Stooq-Primary Data Engine + HF Integration") +st.sidebar.title("Settings") +vol_target = st.sidebar.slider("Annual Vol Target", 0.05, 0.25, 0.126) -# Sidebar Controls -st.sidebar.header("Parameters") -initial_capital = st.sidebar.number_input("Initial Capital", value=100000) -vol_target = st.sidebar.slider("Target Volatility", 0.05, 0.30, 0.15) -lookback = st.sidebar.slider("Lookback (Days)", 50, 300, 200) +if st.sidebar.button("🔄 Refresh Market Data"): + refresh_market_data() + st.sidebar.success("Data Updated from Stooq/SOFR!") -st.sidebar.markdown("---") -st.sidebar.header("Hugging Face Sync") -hf_repo = st.sidebar.text_input("Repo ID", placeholder="user/dataset-name") -hf_token = st.sidebar.text_input("HF Token", type="password") - -run_button = st.sidebar.button("â–ļ Run Full Process") - -if run_button: - from data.loader import load_data, push_to_hf - from engine.backtest import run_backtest - from analytics.metrics import compute_metrics - - # Phase 1: Data Fetching - with st.spinner("Fetching data from Stooq..."): - df = load_data() +if st.button("â–ļ Run All Modules"): + data = pd.read_csv("market_data.csv", index_col=0, parse_dates=True) - if not df.empty: - st.subheader("📈 Market Data Preview") - st.dataframe(df.tail(5), use_container_width=True) - - # Phase 2: Backtesting - with st.spinner("Calculating Trend Strategy..."): - results = run_backtest(df, initial_capital, vol_target, lookback) - metrics = compute_metrics(results["returns"]) - - # Display Results - st.success("Analysis Complete") - c1, c2, c3 = st.columns(3) - c1.metric("CAGR", f"{metrics['cagr']:.2%}") - c2.metric("Sharpe", f"{metrics['sharpe']:.2f}") - c3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") - - st.line_chart(results["equity_curve"]) - - # Phase 3: HF Sync - if hf_repo and hf_token: - with st.spinner("Syncing to Hugging Face..."): - push_to_hf(df, hf_repo, hf_token) - st.sidebar.success("✅ Dataset Synced!") - else: - st.error("Data fetch failed. Verify ticker symbols.") + # Run Modules + eq_res = run_trend_module(data[X_EQUITY_TICKERS], data['SOFR_ANNUAL'], vol_target) + fi_res = run_trend_module(data[FI_TICKERS], data['SOFR_ANNUAL'], vol_target) + + # Performance Comparison + spy_curve = (1 + data['SPY'].pct_change()).cumprod() + comparison = pd.DataFrame({ + "X-ETF Strategy": eq_res['curve'], + "SPY Benchmark": spy_curve + }).dropna() + + st.header("📈 Performance: Equity Strategy vs. SPY") + st.line_chart(comparison) + + # Target Allocations + col1, col2 = st.columns(2) + with col1: + st.subheader("đŸ›Ąī¸ Equity Allocation (Next Day)") + st.dataframe(eq_res['alloc'][eq_res['alloc']['Weight (%)'] > 0]) + with col2: + st.subheader("đŸĻ FI Comparison Allocation") + st.dataframe(fi_res['alloc'][fi_res['alloc']['Weight (%)'] > 0]) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index 8d61a9376d9b5add40b9fa6566ba0a29dfbade3e..fdbf82ba4101d3e404ed0d1733aa4022f49ebf01 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,56 +1,32 @@ -import pandas as pd import pandas_datareader.data as web import yfinance as yf -from datasets import Dataset +import pandas as pd import streamlit as st -from datetime import datetime -# Combined Universe (All will attempt Stooq first) -TICKERS = ["SPY", "QQQ", "IWM", "TLT", "IEF", "SHY", "GLD"] +# 27 "X-" EQUITY ETFS +X_EQUITY_TICKERS = [ + "XLK", "XLY", "XLP", "XLE", "XLV", "XLI", "XLB", "XLRE", "XLU", "XLC", "XLF", + "XBI", "XME", "XOP", "XHB", "XSD", "XRT", "XPH", "XES", "XAR", "XHS", "XHE", + "XSW", "XTN", "XTL", "XNTK", "XITK" +] -def load_data(tickers=TICKERS): - """Fetches data from Stooq with yfinance fallback.""" - all_series = {} +# 15 FIXED INCOME / COMPARISON +FI_TICKERS = ["TLT", "IEF", "TIP", "TBT", "GLD", "SLV", "VGIT", "VCLT", "VCIT", "HYG", "PFF", "MBB", "VNQ", "LQD", "AGG"] - for ticker in tickers: - success = False - # 1. Primary: Stooq +def refresh_market_data(): + """Syncs Stooq/FRED data to local CSV and HF.""" + all_prices = {} + # Download all groups + SPY Benchmark + for t in list(set(X_EQUITY_TICKERS + FI_TICKERS + ["SPY"])): try: - # Stooq format: TICKER.US (e.g., TLT.US) - stooq_symbol = f"{ticker}.US" - df_stooq = web.DataReader(stooq_symbol, 'stooq') + all_prices[t] = web.DataReader(f"{t}.US", "stooq")['Close'] + except: + all_prices[t] = yf.download(t, progress=False)['Adj Close'] - if not df_stooq.empty: - # Stooq returns newest data first; sort to ascending for backtests - all_series[ticker] = df_stooq['Close'].sort_index() - st.toast(f"✅ {ticker} loaded from Stooq") - success = True - except Exception as e: - print(f"Stooq failed for {ticker}: {e}") - - # 2. Fallback: yfinance - if not success: - try: - yf_df = yf.download(ticker, period="max", progress=False) - if not yf_df.empty: - # Use Adj Close to account for dividends/splits - all_series[ticker] = yf_df['Adj Close'] - st.toast(f"âš ī¸ {ticker} loaded from yfinance (Fallback)") - success = True - except Exception as e: - st.error(f"❌ Critical: Could not load {ticker} from any source.") - - if all_series: - # Align all tickers on the same dates and drop missing values - return pd.concat(all_series, axis=1).dropna() - return pd.DataFrame() - -def push_to_hf(df, repo_id, token): - """Pushes the current dataframe to Hugging Face Hub.""" - # Ensure Date is a column, not an index, for HF compatibility - hf_export = df.reset_index() - hf_export.columns = [str(col) for col in hf_export.columns] # Ensure string columns + # Fetch SOFR (Cash Yield) from FRED + sofr = web.DataReader('SOFR', 'fred').ffill() - dataset = Dataset.from_pandas(hf_export) - dataset.push_to_hub(repo_id, token=token) - return True + df = pd.DataFrame(all_prices).sort_index().ffill() + df['SOFR_ANNUAL'] = sofr / 100 + df.to_csv("market_data.csv") + return df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md index c571620450ee0aefd6e05bfa3d1141e69f691ae4..1817b1ac4751066c5cee3fc1b3a3c5a7a50c7a77 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md @@ -1,19 +1,15 @@ --- -title: P2 ETF TREND SUITE -emoji: 🚀 -colorFrom: red -colorTo: red +title: P2 ETF Trend Suite +emoji: 📊 +colorFrom: blue +colorTo: indigo sdk: docker -app_port: 8501 -tags: -- streamlit +app_port: 7860 pinned: false -short_description: Streamlit template space --- -# Welcome to Streamlit! +# 📊 P2 ETF Trend Suite +Institutional ETF Trend + Volatility Targeting Engine. -Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart: - -If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community -forums](https://discuss.streamlit.io). +### 🚀 Setup Info +This Space runs a Dockerized Streamlit app. It uses **Stooq** for market data with **yfinance** as a fallback. diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile index 82c92244713b3df072dd26346b589f1bccbcf1fa..9724d10731e201046939ae744ab6b9db13db0ba2 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile @@ -1,26 +1,31 @@ -FROM python:3.10 +# Use a lightweight but stable Python base +FROM python:3.10-slim + +# Set environment variables for speed and logging +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 WORKDIR /app -# Copy requirements first (better layer caching) +# Install system dependencies needed for pandas/datareader +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy only requirements first to leverage Docker cache COPY requirements.txt . -RUN pip install --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt +# Install dependencies (use --no-cache-dir to keep image small) +RUN pip install --upgrade pip && \ + pip install -r requirements.txt -# Copy full project +# Copy the rest of the application COPY . . +# Ensure the app runs on the port HF expects (7860 for Docker) EXPOSE 7860 -ENV STREAMLIT_SERVER_PORT=7860 -ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 -ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false - -# Diagnostic startup command -CMD ["bash", "-c", "echo '===== CONTAINER BOOTING ====='; \ -echo 'Python Version:'; python -V; \ -echo 'Current Directory:'; pwd; \ -echo 'Directory Listing:'; ls -la; \ -echo 'Starting Streamlit...'; \ -python -m streamlit run app.py --server.headless=true"] +# Correct entrypoint for Streamlit in a container +ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"] diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/__init__.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/engine/__init__.py @@ -0,0 +1 @@ + diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/data/__init__.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/data/__init__.py @@ -0,0 +1 @@ + diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/analytics/__init__.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/analytics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/analytics/__init__.py @@ -0,0 +1 @@ + diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 7db2ad2d93446fe2a9d1d9df77c9e11e5b103daa..bece99ec6fd21a872c830a3bb47c0bf22f7ce199 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,80 +1,54 @@ import streamlit as st -# ===================================================== -# PAGE CONFIG (ONLY LIGHT CODE HERE) -# ===================================================== - -st.set_page_config( - page_title="P2 ETF Trend Suite", - layout="wide", -) +st.set_page_config(page_title="P2 ETF Trend Suite", layout="wide") st.title("📊 P2 ETF Trend Suite") -st.markdown("Institutional ETF Trend + Volatility Targeting Engine") - -# ===================================================== -# SIDEBAR -# ===================================================== - -st.sidebar.header("Strategy Controls") - -initial_capital = st.sidebar.number_input( - "Initial Capital", - value=100000, - step=10000, -) +st.markdown("Stooq-Primary Data Engine + HF Integration") -vol_target = st.sidebar.slider( - "Target Annual Volatility", - 0.05, 0.30, 0.15 -) - -lookback = st.sidebar.slider( - "Momentum Lookback (days)", - 50, 300, 200 -) - -run_button = st.sidebar.button("â–ļ Run Backtest") +# Sidebar Controls +st.sidebar.header("Parameters") +initial_capital = st.sidebar.number_input("Initial Capital", value=100000) +vol_target = st.sidebar.slider("Target Volatility", 0.05, 0.30, 0.15) +lookback = st.sidebar.slider("Lookback (Days)", 50, 300, 200) st.sidebar.markdown("---") -st.sidebar.info("Backtest runs only when button is pressed.") +st.sidebar.header("Hugging Face Sync") +hf_repo = st.sidebar.text_input("Repo ID", placeholder="user/dataset-name") +hf_token = st.sidebar.text_input("HF Token", type="password") -# ===================================================== -# EXECUTION BLOCK -# ===================================================== +run_button = st.sidebar.button("â–ļ Run Full Process") if run_button: + from data.loader import load_data, push_to_hf + from engine.backtest import run_backtest + from analytics.metrics import compute_metrics - with st.spinner("Loading engine..."): - - # Lazy imports happen HERE - from engine.backtest import run_backtest - from data.loader import load_data - from analytics.metrics import compute_metrics - - with st.spinner("Loading market data..."): + # Phase 1: Data Fetching + with st.spinner("Fetching data from Stooq..."): df = load_data() - - with st.spinner("Running strategy..."): - results = run_backtest( - df=df, - initial_capital=initial_capital, - vol_target=vol_target, - lookback=lookback, - ) - - metrics = compute_metrics(results["returns"]) - - st.success("Backtest Complete") - - col1, col2, col3, col4 = st.columns(4) - col1.metric("CAGR", f"{metrics['cagr']:.2%}") - col2.metric("Sharpe", f"{metrics['sharpe']:.2f}") - col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") - col4.metric("Volatility", f"{metrics['vol']:.2%}") - - st.subheader("Equity Curve") - st.line_chart(results["equity_curve"]) - -else: - st.info("Configure parameters and click Run Backtest.") + + if not df.empty: + st.subheader("📈 Market Data Preview") + st.dataframe(df.tail(5), use_container_width=True) + + # Phase 2: Backtesting + with st.spinner("Calculating Trend Strategy..."): + results = run_backtest(df, initial_capital, vol_target, lookback) + metrics = compute_metrics(results["returns"]) + + # Display Results + st.success("Analysis Complete") + c1, c2, c3 = st.columns(3) + c1.metric("CAGR", f"{metrics['cagr']:.2%}") + c2.metric("Sharpe", f"{metrics['sharpe']:.2f}") + c3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") + + st.line_chart(results["equity_curve"]) + + # Phase 3: HF Sync + if hf_repo and hf_token: + with st.spinner("Syncing to Hugging Face..."): + push_to_hf(df, hf_repo, hf_token) + st.sidebar.success("✅ Dataset Synced!") + else: + st.error("Data fetch failed. Verify ticker symbols.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py index a76b4304287b6a19c2be7d3cd453573e4870d0d8..8d61a9376d9b5add40b9fa6566ba0a29dfbade3e 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -1,15 +1,56 @@ -def load_data(): - import pandas as pd - import yfinance as yf +import pandas as pd +import pandas_datareader.data as web +import yfinance as yf +from datasets import Dataset +import streamlit as st +from datetime import datetime - tickers = ["SPY", "QQQ", "TLT"] +# Combined Universe (All will attempt Stooq first) +TICKERS = ["SPY", "QQQ", "IWM", "TLT", "IEF", "SHY", "GLD"] - data = yf.download( - tickers, - start="2015-01-01", - progress=False, - )["Adj Close"] +def load_data(tickers=TICKERS): + """Fetches data from Stooq with yfinance fallback.""" + all_series = {} - data = data.dropna() + for ticker in tickers: + success = False + # 1. Primary: Stooq + try: + # Stooq format: TICKER.US (e.g., TLT.US) + stooq_symbol = f"{ticker}.US" + df_stooq = web.DataReader(stooq_symbol, 'stooq') + + if not df_stooq.empty: + # Stooq returns newest data first; sort to ascending for backtests + all_series[ticker] = df_stooq['Close'].sort_index() + st.toast(f"✅ {ticker} loaded from Stooq") + success = True + except Exception as e: + print(f"Stooq failed for {ticker}: {e}") - return data + # 2. Fallback: yfinance + if not success: + try: + yf_df = yf.download(ticker, period="max", progress=False) + if not yf_df.empty: + # Use Adj Close to account for dividends/splits + all_series[ticker] = yf_df['Adj Close'] + st.toast(f"âš ī¸ {ticker} loaded from yfinance (Fallback)") + success = True + except Exception as e: + st.error(f"❌ Critical: Could not load {ticker} from any source.") + + if all_series: + # Align all tickers on the same dates and drop missing values + return pd.concat(all_series, axis=1).dropna() + return pd.DataFrame() + +def push_to_hf(df, repo_id, token): + """Pushes the current dataframe to Hugging Face Hub.""" + # Ensure Date is a column, not an index, for HF compatibility + hf_export = df.reset_index() + hf_export.columns = [str(col) for col in hf_export.columns] # Ensure string columns + + dataset = Dataset.from_pandas(hf_export) + dataset.push_to_hub(repo_id, token=token) + return True diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile index 8f51b9f478c983ec0d96e445d44e7917612d2b79..82c92244713b3df072dd26346b589f1bccbcf1fa 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile @@ -2,11 +2,13 @@ FROM python:3.10 WORKDIR /app +# Copy requirements first (better layer caching) COPY requirements.txt . RUN pip install --upgrade pip RUN pip install --no-cache-dir -r requirements.txt +# Copy full project COPY . . EXPOSE 7860 @@ -15,4 +17,10 @@ ENV STREAMLIT_SERVER_PORT=7860 ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false -CMD streamlit run app.py +# Diagnostic startup command +CMD ["bash", "-c", "echo '===== CONTAINER BOOTING ====='; \ +echo 'Python Version:'; python -V; \ +echo 'Current Directory:'; pwd; \ +echo 'Directory Listing:'; ls -la; \ +echo 'Starting Streamlit...'; \ +python -m streamlit run app.py --server.headless=true"] diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..a76b4304287b6a19c2be7d3cd453573e4870d0d8 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/loader.py @@ -0,0 +1,15 @@ +def load_data(): + import pandas as pd + import yfinance as yf + + tickers = ["SPY", "QQQ", "TLT"] + + data = yf.download( + tickers, + start="2015-01-01", + progress=False, + )["Adj Close"] + + data = data.dropna() + + return data diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py index e8e5888d2346d958ef68df6c9d21dedb6e433d33..f4f4494f19a1f479a3f1f368c04296ea247c420d 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py @@ -1,26 +1,21 @@ -import numpy as np -import pandas as pd +def compute_metrics(returns): -def compute_metrics(returns, sofr): + import numpy as np - sofr_daily = sofr.reindex(returns.index).fillna(method="ffill")["sofr"] / 252 - excess = returns - sofr_daily + ann_factor = 252 - sharpe = np.sqrt(252) * excess.mean() / excess.std() + cagr = (1 + returns.mean()) ** ann_factor - 1 + vol = returns.std() * (ann_factor ** 0.5) + sharpe = cagr / vol if vol != 0 else 0 - equity = (1 + returns).cumprod() - cagr = equity.iloc[-1] ** (252 / len(equity)) - 1 - - vol = returns.std() * np.sqrt(252) - - rolling_max = equity.cummax() - drawdown = equity / rolling_max - 1 + cumulative = (1 + returns).cumprod() + peak = cumulative.cummax() + drawdown = (cumulative - peak) / peak max_dd = drawdown.min() return { - "sharpe": sharpe, "cagr": cagr, "vol": vol, - "max_dd": max_dd + "sharpe": sharpe, + "max_dd": max_dd, } - diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py index 3cf746a2e564a42893ab1ee7101f7b944eb322a5..a9c0aed248148b72044c880828766352daf18659 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py @@ -1,46 +1,20 @@ -import pandas as pd -import numpy as np - def run_backtest(df, initial_capital, vol_target, lookback): - df = df.sort_values(["ticker", "date"]) - prices = df.pivot(index="date", columns="ticker", values="adjusted_close") - returns = prices.pct_change().dropna() - - momentum = prices.pct_change(lookback) - signal = momentum.rank(axis=1, ascending=False) - top = signal <= 3 - - weights = top.div(top.sum(axis=1), axis=0) - - rolling_cov = returns.rolling(60).cov() - vol = [] + import numpy as np + import pandas as pd - for date in weights.index: - if date not in rolling_cov.index: - vol.append(0) - continue + returns = df.pct_change().dropna() - w = weights.loc[date].values - cov = rolling_cov.loc[date].values.reshape(len(w), len(w)) - portfolio_vol = np.sqrt(w @ cov @ w) * np.sqrt(252) + momentum = df.pct_change(lookback) - scale = vol_target / portfolio_vol if portfolio_vol > 0 else 0 - weights.loc[date] = w * scale - vol.append(portfolio_vol) + weights = (momentum > 0).astype(int) + weights = weights.div(weights.sum(axis=1), axis=0).fillna(0) strategy_returns = (weights.shift(1) * returns).sum(axis=1) - equity_curve = (1 + strategy_returns).cumprod() * initial_capital - latest_weights = weights.iloc[-1] - allocation = pd.DataFrame({ - "Ticker": latest_weights.index, - "Weight": latest_weights.values - }).sort_values("Weight", ascending=False) + equity_curve = (1 + strategy_returns).cumprod() * initial_capital return { "returns": strategy_returns, "equity_curve": equity_curve, - "latest_allocation": allocation } - diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 980decdea60b6cc75e90d9c75f9e5b66d3b08949..7db2ad2d93446fe2a9d1d9df77c9e11e5b103daa 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,7 +1,7 @@ import streamlit as st # ===================================================== -# PAGE CONFIG (must be first Streamlit command) +# PAGE CONFIG (ONLY LIGHT CODE HERE) # ===================================================== st.set_page_config( @@ -13,7 +13,7 @@ st.title("📊 P2 ETF Trend Suite") st.markdown("Institutional ETF Trend + Volatility Targeting Engine") # ===================================================== -# SIDEBAR CONTROLS +# SIDEBAR # ===================================================== st.sidebar.header("Strategy Controls") @@ -26,16 +26,12 @@ initial_capital = st.sidebar.number_input( vol_target = st.sidebar.slider( "Target Annual Volatility", - min_value=0.05, - max_value=0.30, - value=0.15, + 0.05, 0.30, 0.15 ) lookback = st.sidebar.slider( "Momentum Lookback (days)", - min_value=50, - max_value=300, - value=200, + 50, 300, 200 ) run_button = st.sidebar.button("â–ļ Run Backtest") @@ -44,34 +40,22 @@ st.sidebar.markdown("---") st.sidebar.info("Backtest runs only when button is pressed.") # ===================================================== -# MAIN EXECUTION (runs ONLY when button clicked) +# EXECUTION BLOCK # ===================================================== if run_button: - # Import heavy modules only when needed - from data.hf_store import load_dataset - from data.updater import update_market_data - from data.fred import get_sofr_series - from engine.backtest import run_backtest - from analytics.metrics import compute_metrics - - # --------------------------- - # Load Dataset - # --------------------------- - with st.spinner("Loading ETF dataset from Hugging Face..."): - df = load_dataset() - - # --------------------------- - # Pull SOFR - # --------------------------- - with st.spinner("Pulling SOFR from FRED..."): - sofr = get_sofr_series() - - # --------------------------- - # Run Backtest - # --------------------------- - with st.spinner("Running backtest engine..."): + with st.spinner("Loading engine..."): + + # Lazy imports happen HERE + from engine.backtest import run_backtest + from data.loader import load_data + from analytics.metrics import compute_metrics + + with st.spinner("Loading market data..."): + df = load_data() + + with st.spinner("Running strategy..."): results = run_backtest( df=df, initial_capital=initial_capital, @@ -79,38 +63,18 @@ if run_button: lookback=lookback, ) - metrics = compute_metrics(results["returns"], sofr) + metrics = compute_metrics(results["returns"]) st.success("Backtest Complete") - # ===================================================== - # METRICS PANEL - # ===================================================== - col1, col2, col3, col4 = st.columns(4) - col1.metric("CAGR", f"{metrics['cagr']:.2%}") - col2.metric("Sharpe (SOFR)", f"{metrics['sharpe']:.2f}") + col2.metric("Sharpe", f"{metrics['sharpe']:.2f}") col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") col4.metric("Volatility", f"{metrics['vol']:.2%}") - st.markdown("---") - - # ===================================================== - # EQUITY CURVE - # ===================================================== - st.subheader("Equity Curve") st.line_chart(results["equity_curve"]) - st.markdown("---") - - # ===================================================== - # ALLOCATION TABLE - # ===================================================== - - st.subheader("Latest Portfolio Allocation") - st.dataframe(results["latest_allocation"], use_container_width=True) - else: - st.info("Configure parameters in the sidebar and click **Run Backtest**.") + st.info("Configure parameters and click Run Backtest.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile index bb3931c783bea026eefd60555fce13c6cc877681..8f51b9f478c983ec0d96e445d44e7917612d2b79 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile @@ -1,13 +1,18 @@ -FROM python:3.10-slim +FROM python:3.10 WORKDIR /app COPY requirements.txt . +RUN pip install --upgrade pip RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 7860 -CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"] +ENV STREAMLIT_SERVER_PORT=7860 +ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 +ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false + +CMD streamlit run app.py diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 2f8ce9d33ba2c163b56cfd50e515d779360b2791..980decdea60b6cc75e90d9c75f9e5b66d3b08949 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,70 +1,116 @@ import streamlit as st -import os -from data.hf_store import load_dataset -from data.updater import update_market_data -from data.fred import get_sofr_series -from engine.backtest import run_backtest -from analytics.metrics import compute_metrics +# ===================================================== +# PAGE CONFIG (must be first Streamlit command) +# ===================================================== -st.set_page_config(layout="wide") +st.set_page_config( + page_title="P2 ETF Trend Suite", + layout="wide", +) st.title("📊 P2 ETF Trend Suite") st.markdown("Institutional ETF Trend + Volatility Targeting Engine") -# ======================== -# Sidebar Controls -# ======================== +# ===================================================== +# SIDEBAR CONTROLS +# ===================================================== -st.sidebar.header("Controls") +st.sidebar.header("Strategy Controls") -initial_capital = st.sidebar.number_input("Initial Capital", value=100000, step=10000) -vol_target = st.sidebar.slider("Target Annual Volatility", 0.05, 0.30, 0.15) -lookback = st.sidebar.slider("Momentum Lookback (days)", 50, 300, 200) +initial_capital = st.sidebar.number_input( + "Initial Capital", + value=100000, + step=10000, +) -refresh = st.sidebar.button("🔄 Refresh Market Data") +vol_target = st.sidebar.slider( + "Target Annual Volatility", + min_value=0.05, + max_value=0.30, + value=0.15, +) -# ======================== -# Data Load -# ======================== +lookback = st.sidebar.slider( + "Momentum Lookback (days)", + min_value=50, + max_value=300, + value=200, +) -with st.spinner("Loading ETF dataset..."): - df = load_dataset() +run_button = st.sidebar.button("â–ļ Run Backtest") -if refresh: - with st.spinner("Updating market data from yfinance..."): - df = update_market_data(df) - st.success("Dataset updated successfully.") +st.sidebar.markdown("---") +st.sidebar.info("Backtest runs only when button is pressed.") -# ======================== -# Backtest -# ======================== +# ===================================================== +# MAIN EXECUTION (runs ONLY when button clicked) +# ===================================================== -with st.spinner("Pulling SOFR from FRED..."): - sofr = get_sofr_series() +if run_button: -with st.spinner("Running backtest..."): - results = run_backtest( - df=df, - initial_capital=initial_capital, - vol_target=vol_target, - lookback=lookback, - ) + # Import heavy modules only when needed + from data.hf_store import load_dataset + from data.updater import update_market_data + from data.fred import get_sofr_series + from engine.backtest import run_backtest + from analytics.metrics import compute_metrics -metrics = compute_metrics(results["returns"], sofr) + # --------------------------- + # Load Dataset + # --------------------------- + with st.spinner("Loading ETF dataset from Hugging Face..."): + df = load_dataset() -# ======================== -# Layout -# ======================== + # --------------------------- + # Pull SOFR + # --------------------------- + with st.spinner("Pulling SOFR from FRED..."): + sofr = get_sofr_series() -col1, col2, col3, col4 = st.columns(4) + # --------------------------- + # Run Backtest + # --------------------------- + with st.spinner("Running backtest engine..."): + results = run_backtest( + df=df, + initial_capital=initial_capital, + vol_target=vol_target, + lookback=lookback, + ) -col1.metric("CAGR", f"{metrics['cagr']:.2%}") -col2.metric("Sharpe (SOFR)", f"{metrics['sharpe']:.2f}") -col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") -col4.metric("Volatility", f"{metrics['vol']:.2%}") + metrics = compute_metrics(results["returns"], sofr) -st.line_chart(results["equity_curve"]) + st.success("Backtest Complete") -st.subheader("Current Allocation") -st.dataframe(results["latest_allocation"]) + # ===================================================== + # METRICS PANEL + # ===================================================== + + col1, col2, col3, col4 = st.columns(4) + + col1.metric("CAGR", f"{metrics['cagr']:.2%}") + col2.metric("Sharpe (SOFR)", f"{metrics['sharpe']:.2f}") + col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") + col4.metric("Volatility", f"{metrics['vol']:.2%}") + + st.markdown("---") + + # ===================================================== + # EQUITY CURVE + # ===================================================== + + st.subheader("Equity Curve") + st.line_chart(results["equity_curve"]) + + st.markdown("---") + + # ===================================================== + # ALLOCATION TABLE + # ===================================================== + + st.subheader("Latest Portfolio Allocation") + st.dataframe(results["latest_allocation"], use_container_width=True) + +else: + st.info("Configure parameters in the sidebar and click **Run Backtest**.") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile index 5f51ead59f36f13043e036290df9440e25fe8cbe..bb3931c783bea026eefd60555fce13c6cc877681 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile @@ -1,20 +1,13 @@ -FROM python:3.13.5-slim +FROM python:3.10-slim WORKDIR /app -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - git \ - && rm -rf /var/lib/apt/lists/* +COPY requirements.txt . -COPY requirements.txt ./ -COPY src/ ./src/ +RUN pip install --no-cache-dir -r requirements.txt -RUN pip3 install -r requirements.txt +COPY . . -EXPOSE 8501 +EXPOSE 7860 -HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health - -ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file +CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"] diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py index 91487ec3342b010a22a9da2fba99b9249e9d42c7..2f8ce9d33ba2c163b56cfd50e515d779360b2791 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -1,35 +1,70 @@ import streamlit as st +import os + +from data.hf_store import load_dataset +from data.updater import update_market_data +from data.fred import get_sofr_series +from engine.backtest import run_backtest +from analytics.metrics import compute_metrics st.set_page_config(layout="wide") -st.title("P2 ETF Trend Suite - Debug Mode") - -try: - from data.hf_store import load_dataset - st.success("hf_store imported successfully") -except Exception as e: - st.error(f"hf_store failed: {e}") - -try: - from data.updater import update_market_data - st.success("updater imported successfully") -except Exception as e: - st.error(f"updater failed: {e}") - -try: - from data.fred import get_sofr_series - st.success("fred imported successfully") -except Exception as e: - st.error(f"fred failed: {e}") - -try: - from engine.backtest import run_backtest - st.success("backtest imported successfully") -except Exception as e: - st.error(f"backtest failed: {e}") - -try: - from analytics.metrics import compute_metrics - st.success("metrics imported successfully") -except Exception as e: - st.error(f"metrics failed: {e}") +st.title("📊 P2 ETF Trend Suite") +st.markdown("Institutional ETF Trend + Volatility Targeting Engine") + +# ======================== +# Sidebar Controls +# ======================== + +st.sidebar.header("Controls") + +initial_capital = st.sidebar.number_input("Initial Capital", value=100000, step=10000) +vol_target = st.sidebar.slider("Target Annual Volatility", 0.05, 0.30, 0.15) +lookback = st.sidebar.slider("Momentum Lookback (days)", 50, 300, 200) + +refresh = st.sidebar.button("🔄 Refresh Market Data") + +# ======================== +# Data Load +# ======================== + +with st.spinner("Loading ETF dataset..."): + df = load_dataset() + +if refresh: + with st.spinner("Updating market data from yfinance..."): + df = update_market_data(df) + st.success("Dataset updated successfully.") + +# ======================== +# Backtest +# ======================== + +with st.spinner("Pulling SOFR from FRED..."): + sofr = get_sofr_series() + +with st.spinner("Running backtest..."): + results = run_backtest( + df=df, + initial_capital=initial_capital, + vol_target=vol_target, + lookback=lookback, + ) + +metrics = compute_metrics(results["returns"], sofr) + +# ======================== +# Layout +# ======================== + +col1, col2, col3, col4 = st.columns(4) + +col1.metric("CAGR", f"{metrics['cagr']:.2%}") +col2.metric("Sharpe (SOFR)", f"{metrics['sharpe']:.2f}") +col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") +col4.metric("Volatility", f"{metrics['vol']:.2%}") + +st.line_chart(results["equity_curve"]) + +st.subheader("Current Allocation") +st.dataframe(results["latest_allocation"]) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..e8e5888d2346d958ef68df6c9d21dedb6e433d33 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/analytics/metrics.py @@ -0,0 +1,26 @@ +import numpy as np +import pandas as pd + +def compute_metrics(returns, sofr): + + sofr_daily = sofr.reindex(returns.index).fillna(method="ffill")["sofr"] / 252 + excess = returns - sofr_daily + + sharpe = np.sqrt(252) * excess.mean() / excess.std() + + equity = (1 + returns).cumprod() + cagr = equity.iloc[-1] ** (252 / len(equity)) - 1 + + vol = returns.std() * np.sqrt(252) + + rolling_max = equity.cummax() + drawdown = equity / rolling_max - 1 + max_dd = drawdown.min() + + return { + "sharpe": sharpe, + "cagr": cagr, + "vol": vol, + "max_dd": max_dd + } + diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py new file mode 100644 index 0000000000000000000000000000000000000000..91487ec3342b010a22a9da2fba99b9249e9d42c7 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/app.py @@ -0,0 +1,35 @@ +import streamlit as st + +st.set_page_config(layout="wide") + +st.title("P2 ETF Trend Suite - Debug Mode") + +try: + from data.hf_store import load_dataset + st.success("hf_store imported successfully") +except Exception as e: + st.error(f"hf_store failed: {e}") + +try: + from data.updater import update_market_data + st.success("updater imported successfully") +except Exception as e: + st.error(f"updater failed: {e}") + +try: + from data.fred import get_sofr_series + st.success("fred imported successfully") +except Exception as e: + st.error(f"fred failed: {e}") + +try: + from engine.backtest import run_backtest + st.success("backtest imported successfully") +except Exception as e: + st.error(f"backtest failed: {e}") + +try: + from analytics.metrics import compute_metrics + st.success("metrics imported successfully") +except Exception as e: + st.error(f"metrics failed: {e}") diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/fred.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/fred.py new file mode 100644 index 0000000000000000000000000000000000000000..060d85abd48d015dcdb9f7d1ff09d804f0739e35 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/fred.py @@ -0,0 +1,11 @@ +import os +import pandas as pd +from fredapi import Fred + +def get_sofr_series(): + fred = Fred(api_key=os.getenv("FRED_API_KEY")) + sofr = fred.get_series("SOFR") + sofr = sofr.to_frame("sofr") + sofr.index = pd.to_datetime(sofr.index) + sofr["sofr"] = sofr["sofr"] / 100 + return sofr diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/hf_store.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/hf_store.py new file mode 100644 index 0000000000000000000000000000000000000000..d9e4f70959dd855e9ba41ebf4b0a50955528e08c --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/hf_store.py @@ -0,0 +1,17 @@ +import pandas as pd +from datasets import load_dataset as hf_load_dataset + +DATASET_PATH = "P2SAMAPA/etf_trend_data" + +def load_dataset(): + dataset = hf_load_dataset(DATASET_PATH) + + # Handle split safely + if isinstance(dataset, dict) and "train" in dataset: + dataset = dataset["train"] + + df = dataset.to_pandas() + df["date"] = pd.to_datetime(df["date"]) + df = df.sort_values(["ticker", "date"]) + + return df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/updater.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/updater.py new file mode 100644 index 0000000000000000000000000000000000000000..1a8e05f0bbd4aa1571b26bbec0d19800fb197cef --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/data/updater.py @@ -0,0 +1,55 @@ +import os +import pandas as pd +import yfinance as yf +from huggingface_hub import HfApi + +DATASET_PATH = "P2SAMAPA/etf_trend_data" + +def update_market_data(df): + + tickers = df["ticker"].unique() + all_new = [] + + for ticker in tickers: + last_date = df[df["ticker"] == ticker]["date"].max() + start_date = (last_date + pd.Timedelta(days=1)).strftime("%Y-%m-%d") + + new_data = yf.download(ticker, start=start_date, progress=False) + + if new_data.empty: + continue + + new_data.reset_index(inplace=True) + new_data["ticker"] = ticker + new_data.rename(columns={ + "Date": "date", + "Open": "open", + "High": "high", + "Low": "low", + "Close": "close", + "Adj Close": "adjusted_close", + "Volume": "volume" + }, inplace=True) + + all_new.append(new_data) + + if not all_new: + return df + + new_df = pd.concat(all_new) + df = pd.concat([df, new_df]) + df.drop_duplicates(subset=["date", "ticker"], inplace=True) + df.sort_values(["ticker", "date"], inplace=True) + + df.to_parquet("updated.parquet") + + api = HfApi() + api.upload_file( + path_or_fileobj="updated.parquet", + path_in_repo="data/train-00000-of-00001.parquet", + repo_id=DATASET_PATH, + repo_type="dataset", + token=os.getenv("HF_TOKEN") + ) + + return df diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py new file mode 100644 index 0000000000000000000000000000000000000000..3cf746a2e564a42893ab1ee7101f7b944eb322a5 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/engine/backtest.py @@ -0,0 +1,46 @@ +import pandas as pd +import numpy as np + +def run_backtest(df, initial_capital, vol_target, lookback): + + df = df.sort_values(["ticker", "date"]) + prices = df.pivot(index="date", columns="ticker", values="adjusted_close") + returns = prices.pct_change().dropna() + + momentum = prices.pct_change(lookback) + signal = momentum.rank(axis=1, ascending=False) + top = signal <= 3 + + weights = top.div(top.sum(axis=1), axis=0) + + rolling_cov = returns.rolling(60).cov() + vol = [] + + for date in weights.index: + if date not in rolling_cov.index: + vol.append(0) + continue + + w = weights.loc[date].values + cov = rolling_cov.loc[date].values.reshape(len(w), len(w)) + portfolio_vol = np.sqrt(w @ cov @ w) * np.sqrt(252) + + scale = vol_target / portfolio_vol if portfolio_vol > 0 else 0 + weights.loc[date] = w * scale + vol.append(portfolio_vol) + + strategy_returns = (weights.shift(1) * returns).sum(axis=1) + equity_curve = (1 + strategy_returns).cumprod() * initial_capital + + latest_weights = weights.iloc[-1] + allocation = pd.DataFrame({ + "Ticker": latest_weights.index, + "Weight": latest_weights.values + }).sort_values("Weight", ascending=False) + + return { + "returns": strategy_returns, + "equity_curve": equity_curve, + "latest_allocation": allocation + } + diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/.gitattributes b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5f51ead59f36f13043e036290df9440e25fe8cbe --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.13.5-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt ./ +COPY src/ ./src/ + +RUN pip3 install -r requirements.txt + +EXPOSE 8501 + +HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health + +ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c571620450ee0aefd6e05bfa3d1141e69f691ae4 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/README.md @@ -0,0 +1,19 @@ +--- +title: P2 ETF TREND SUITE +emoji: 🚀 +colorFrom: red +colorTo: red +sdk: docker +app_port: 8501 +tags: +- streamlit +pinned: false +short_description: Streamlit template space +--- + +# Welcome to Streamlit! + +Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart: + +If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community +forums](https://discuss.streamlit.io). diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f192dfc876989523e2d5bb36a77336030d50527c --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt @@ -0,0 +1,8 @@ +streamlit +pandas +numpy +yfinance +datasets +huggingface_hub +fredapi +scipy diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/src/streamlit_app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/src/streamlit_app.py new file mode 100644 index 0000000000000000000000000000000000000000..99d0b84662681e7d21a08fcce44908344fa86f80 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/src/streamlit_app.py @@ -0,0 +1,40 @@ +import altair as alt +import numpy as np +import pandas as pd +import streamlit as st + +""" +# Welcome to Streamlit! + +Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:. +If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community +forums](https://discuss.streamlit.io). + +In the meantime, below is an example of what you can do with just a few lines of code: +""" + +num_points = st.slider("Number of points in spiral", 1, 10000, 1100) +num_turns = st.slider("Number of turns in spiral", 1, 300, 31) + +indices = np.linspace(0, 1, num_points) +theta = 2 * np.pi * num_turns * indices +radius = indices + +x = radius * np.cos(theta) +y = radius * np.sin(theta) + +df = pd.DataFrame({ + "x": x, + "y": y, + "idx": indices, + "rand": np.random.randn(num_points), +}) + +st.altair_chart(alt.Chart(df, height=700, width=700) + .mark_point(filled=True) + .encode( + x=alt.X("x", axis=None), + y=alt.Y("y", axis=None), + color=alt.Color("idx", legend=None, scale=alt.Scale()), + size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])), + )) \ No newline at end of file diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/streamlit_app.py b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/streamlit_app.py new file mode 100644 index 0000000000000000000000000000000000000000..2f8ce9d33ba2c163b56cfd50e515d779360b2791 --- /dev/null +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/streamlit_app.py @@ -0,0 +1,70 @@ +import streamlit as st +import os + +from data.hf_store import load_dataset +from data.updater import update_market_data +from data.fred import get_sofr_series +from engine.backtest import run_backtest +from analytics.metrics import compute_metrics + +st.set_page_config(layout="wide") + +st.title("📊 P2 ETF Trend Suite") +st.markdown("Institutional ETF Trend + Volatility Targeting Engine") + +# ======================== +# Sidebar Controls +# ======================== + +st.sidebar.header("Controls") + +initial_capital = st.sidebar.number_input("Initial Capital", value=100000, step=10000) +vol_target = st.sidebar.slider("Target Annual Volatility", 0.05, 0.30, 0.15) +lookback = st.sidebar.slider("Momentum Lookback (days)", 50, 300, 200) + +refresh = st.sidebar.button("🔄 Refresh Market Data") + +# ======================== +# Data Load +# ======================== + +with st.spinner("Loading ETF dataset..."): + df = load_dataset() + +if refresh: + with st.spinner("Updating market data from yfinance..."): + df = update_market_data(df) + st.success("Dataset updated successfully.") + +# ======================== +# Backtest +# ======================== + +with st.spinner("Pulling SOFR from FRED..."): + sofr = get_sofr_series() + +with st.spinner("Running backtest..."): + results = run_backtest( + df=df, + initial_capital=initial_capital, + vol_target=vol_target, + lookback=lookback, + ) + +metrics = compute_metrics(results["returns"], sofr) + +# ======================== +# Layout +# ======================== + +col1, col2, col3, col4 = st.columns(4) + +col1.metric("CAGR", f"{metrics['cagr']:.2%}") +col2.metric("Sharpe (SOFR)", f"{metrics['sharpe']:.2f}") +col3.metric("Max Drawdown", f"{metrics['max_dd']:.2%}") +col4.metric("Volatility", f"{metrics['vol']:.2%}") + +st.line_chart(results["equity_curve"]) + +st.subheader("Current Allocation") +st.dataframe(results["latest_allocation"]) diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt index f192dfc876989523e2d5bb36a77336030d50527c..2ee50822b0d35d13e955b7b90f1b6006f603d6cb 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt @@ -1,8 +1,6 @@ streamlit pandas -numpy yfinance +pandas-datareader datasets huggingface_hub -fredapi -scipy diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt index 2ee50822b0d35d13e955b7b90f1b6006f603d6cb..1e7c22b61165889689d808bacd2a9fa4d4680a07 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt @@ -1,6 +1,6 @@ streamlit pandas -yfinance pandas-datareader -datasets -huggingface_hub +yfinance +numpy +pandas_market_calendars diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt index 1e7c22b61165889689d808bacd2a9fa4d4680a07..002997e6a6781dc622c55a07b6a1c85c34a30149 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt @@ -4,3 +4,4 @@ pandas-datareader yfinance numpy pandas_market_calendars +huggingface_hub diff --git a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt index 002997e6a6781dc622c55a07b6a1c85c34a30149..7553b8d97272a2c826fd1ddd39e38d16ea29e297 100644 --- a/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt +++ b/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/hf_space/requirements.txt @@ -1,7 +1,9 @@ streamlit pandas +numpy pandas-datareader yfinance -numpy -pandas_market_calendars -huggingface_hub +huggingface-hub +pandas-market-calendars +plotly +scipy