Spaces:
Running
Running
P2SAMAPA commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -72,17 +72,40 @@ def load_training_meta():
|
|
| 72 |
return None
|
| 73 |
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
try:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
repo_type="dataset", force_download=True)
|
| 82 |
with open(path) as f:
|
| 83 |
-
return json.load(f)
|
| 84 |
except Exception:
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
# ── GitHub Actions helpers ────────────────────────────────────────────────────
|
|
@@ -561,26 +584,58 @@ with tab2:
|
|
| 561 |
"**Score:** 40% Return · 20% Z · 20% Sharpe · 20% (–MaxDD)"
|
| 562 |
)
|
| 563 |
|
| 564 |
-
# ──
|
| 565 |
-
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
for yr in SWEEP_YEARS:
|
| 568 |
-
|
| 569 |
-
if
|
| 570 |
-
sweep_cache[yr] =
|
|
|
|
|
|
|
|
|
|
| 571 |
else:
|
| 572 |
missing_years.append(yr)
|
| 573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
# ── Status grid ──────────────────────────────────────────────────────────
|
| 575 |
cols = st.columns(len(SWEEP_YEARS))
|
| 576 |
for i, yr in enumerate(SWEEP_YEARS):
|
| 577 |
with cols[i]:
|
| 578 |
if yr in sweep_cache:
|
| 579 |
sig = sweep_cache[yr]['next_signal']
|
| 580 |
-
st.success(f"**{yr}**\n
|
|
|
|
|
|
|
|
|
|
| 581 |
else:
|
| 582 |
st.error(f"**{yr}**\n⏳ Not run")
|
| 583 |
|
|
|
|
| 584 |
st.divider()
|
| 585 |
|
| 586 |
# ── Sweep button ──────────��───────────────────────────────────────────────
|
|
@@ -590,30 +645,32 @@ with tab2:
|
|
| 590 |
"🚀 Run Consensus Sweep",
|
| 591 |
type="primary",
|
| 592 |
use_container_width=True,
|
| 593 |
-
disabled=(is_training or
|
| 594 |
-
help="
|
| 595 |
)
|
| 596 |
with col_info:
|
| 597 |
-
if
|
| 598 |
-
st.success("✅
|
| 599 |
elif is_training:
|
| 600 |
-
st.warning(f"⏳ Training in progress... ({len(sweep_cache)}/{len(SWEEP_YEARS)}
|
| 601 |
else:
|
| 602 |
-
st.info(
|
| 603 |
-
|
| 604 |
-
|
|
|
|
|
|
|
| 605 |
|
| 606 |
-
if sweep_btn and
|
| 607 |
-
sweep_mode_str = ",".join(str(y) for y in
|
| 608 |
with st.spinner(f"🚀 Triggering parallel training for: {sweep_mode_str}..."):
|
| 609 |
ok = trigger_github_training(
|
| 610 |
-
start_year=
|
| 611 |
sweep_mode=sweep_mode_str,
|
| 612 |
force_refresh=False
|
| 613 |
)
|
| 614 |
if ok:
|
| 615 |
st.success(
|
| 616 |
-
f"✅ Triggered **{len(
|
| 617 |
f"Each takes ~90 mins. Refresh this tab when complete."
|
| 618 |
)
|
| 619 |
time.sleep(2)
|
|
@@ -622,11 +679,11 @@ with tab2:
|
|
| 622 |
st.error("❌ Failed to trigger GitHub Actions sweep.")
|
| 623 |
|
| 624 |
# ── Consensus results ─────────────────────────────────────────────────────
|
| 625 |
-
if len(
|
| 626 |
st.info("👆 Click **🚀 Run Consensus Sweep** to train all years.")
|
| 627 |
st.stop()
|
| 628 |
|
| 629 |
-
consensus = compute_consensus(
|
| 630 |
if not consensus:
|
| 631 |
st.warning("⚠️ Could not compute consensus.")
|
| 632 |
st.stop()
|
|
@@ -635,7 +692,7 @@ with tab2:
|
|
| 635 |
w_info = consensus['etf_summary'][winner]
|
| 636 |
win_color = ETF_COLORS.get(winner, "#00d1b2")
|
| 637 |
score_share = w_info['score_share'] * 100
|
| 638 |
-
n_cached = len(
|
| 639 |
|
| 640 |
# ── Consensus winner banner ───────────────────────────────────────────────
|
| 641 |
split_signal = w_info['score_share'] < 0.4
|
|
@@ -648,7 +705,7 @@ with tab2:
|
|
| 648 |
padding:32px;text-align:center;margin:20px 0;
|
| 649 |
box-shadow:0 8px 24px rgba(0,0,0,0.4);">
|
| 650 |
<div style="font-size:11px;letter-spacing:3px;color:#aaa;margin-bottom:12px;">
|
| 651 |
-
WEIGHTED CONSENSUS · TFT · {n_cached} START YEARS
|
| 652 |
</div>
|
| 653 |
<div style="font-size:72px;font-weight:900;color:{win_color};
|
| 654 |
text-shadow:0 0 30px {win_color}88;letter-spacing:2px;">
|
|
@@ -762,7 +819,7 @@ with tab2:
|
|
| 762 |
for row in sorted(consensus['per_year'], key=lambda r: r['year']):
|
| 763 |
etf = row['signal']
|
| 764 |
col = ETF_COLORS.get(etf, "#888")
|
| 765 |
-
|
| 766 |
table_rows.append({
|
| 767 |
'Start Year': row['year'],
|
| 768 |
'Signal': etf,
|
|
@@ -773,7 +830,7 @@ with tab2:
|
|
| 773 |
'Sharpe': f"{row['sharpe']:.2f}",
|
| 774 |
'Max Drawdown': f"{row['max_dd']*100:.2f}%",
|
| 775 |
'Lookback': f"{row['lookback']}d",
|
| 776 |
-
'Cache': "
|
| 777 |
})
|
| 778 |
|
| 779 |
tbl_df = pd.DataFrame(table_rows)
|
|
|
|
| 72 |
return None
|
| 73 |
|
| 74 |
|
| 75 |
+
def _today_est():
|
| 76 |
+
from datetime import timezone
|
| 77 |
+
return (datetime.now(timezone.utc) - timedelta(hours=5)).date()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@st.cache_data(ttl=60)
|
| 81 |
+
def load_sweep_signals(year: int, for_date: str):
|
| 82 |
+
"""Load date-stamped sweep signals. Returns (data, is_today)."""
|
| 83 |
+
from huggingface_hub import hf_hub_download
|
| 84 |
+
date_tag = for_date.replace("-", "")
|
| 85 |
+
|
| 86 |
+
# Try today's file
|
| 87 |
try:
|
| 88 |
+
path = hf_hub_download(repo_id=HF_OUTPUT_REPO,
|
| 89 |
+
filename=f"signals_{year}_{date_tag}.json",
|
| 90 |
repo_type="dataset", force_download=True)
|
| 91 |
with open(path) as f:
|
| 92 |
+
return json.load(f), True
|
| 93 |
except Exception:
|
| 94 |
+
pass
|
| 95 |
+
|
| 96 |
+
# Fall back to yesterday's file
|
| 97 |
+
try:
|
| 98 |
+
from datetime import date as _date, timedelta as _td
|
| 99 |
+
yesterday = (_date.fromisoformat(for_date) - _td(days=1)).strftime("%Y%m%d")
|
| 100 |
+
path = hf_hub_download(repo_id=HF_OUTPUT_REPO,
|
| 101 |
+
filename=f"signals_{year}_{yesterday}.json",
|
| 102 |
+
repo_type="dataset", force_download=True)
|
| 103 |
+
with open(path) as f:
|
| 104 |
+
return json.load(f), False
|
| 105 |
+
except Exception:
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
return None, False
|
| 109 |
|
| 110 |
|
| 111 |
# ── GitHub Actions helpers ────────────────────────────────────────────────────
|
|
|
|
| 584 |
"**Score:** 40% Return · 20% Z · 20% Sharpe · 20% (–MaxDD)"
|
| 585 |
)
|
| 586 |
|
| 587 |
+
# ── Date-aware sweep cache loading ───────────────────────────────────────
|
| 588 |
+
today_str = str(_today_est())
|
| 589 |
+
sweep_cache = {} # today's results
|
| 590 |
+
prev_cache = {} # yesterday's results (fallback)
|
| 591 |
+
stale_years = [] # years where only yesterday's data exists
|
| 592 |
+
missing_years = [] # years with no data at all
|
| 593 |
+
|
| 594 |
for yr in SWEEP_YEARS:
|
| 595 |
+
data, is_today = load_sweep_signals(yr, today_str)
|
| 596 |
+
if data and is_today:
|
| 597 |
+
sweep_cache[yr] = data
|
| 598 |
+
elif data and not is_today:
|
| 599 |
+
prev_cache[yr] = data
|
| 600 |
+
stale_years.append(yr)
|
| 601 |
else:
|
| 602 |
missing_years.append(yr)
|
| 603 |
|
| 604 |
+
# Display cache = today's where available, yesterday's as fallback
|
| 605 |
+
display_cache = {**prev_cache, **sweep_cache} # today overrides yesterday
|
| 606 |
+
years_needing_run = [yr for yr in SWEEP_YEARS if yr not in sweep_cache]
|
| 607 |
+
sweep_complete = len(sweep_cache) == len(SWEEP_YEARS)
|
| 608 |
+
|
| 609 |
+
# ── Stale data warning banner ─────────────────────────────────────────────
|
| 610 |
+
if stale_years and not sweep_complete:
|
| 611 |
+
from datetime import date as _d, timedelta as _td
|
| 612 |
+
yesterday = str(_d.fromisoformat(today_str) - _td(days=1))
|
| 613 |
+
st.warning(
|
| 614 |
+
f"⚠️ Showing **yesterday's results** ({yesterday}) for: "
|
| 615 |
+
f"{', '.join(str(y) for y in stale_years)}. "
|
| 616 |
+
f"Today's sweep has not run yet — auto-runs at 8pm EST or click below.",
|
| 617 |
+
icon="📅"
|
| 618 |
+
)
|
| 619 |
+
if is_training and not sweep_complete:
|
| 620 |
+
st.info(
|
| 621 |
+
f"⏳ **Training in progress** — {len(sweep_cache)}/{len(SWEEP_YEARS)} years "
|
| 622 |
+
f"complete today. Showing previous results where available.", icon="🔄"
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
# ── Status grid ──────────────────────────────────────────────────────────
|
| 626 |
cols = st.columns(len(SWEEP_YEARS))
|
| 627 |
for i, yr in enumerate(SWEEP_YEARS):
|
| 628 |
with cols[i]:
|
| 629 |
if yr in sweep_cache:
|
| 630 |
sig = sweep_cache[yr]['next_signal']
|
| 631 |
+
st.success(f"**{yr}**\n✅ {sig}")
|
| 632 |
+
elif yr in prev_cache:
|
| 633 |
+
sig = prev_cache[yr]['next_signal']
|
| 634 |
+
st.warning(f"**{yr}**\n📅 {sig}")
|
| 635 |
else:
|
| 636 |
st.error(f"**{yr}**\n⏳ Not run")
|
| 637 |
|
| 638 |
+
st.caption("✅ = today's result · 📅 = yesterday's result (stale) · ⏳ = not yet run")
|
| 639 |
st.divider()
|
| 640 |
|
| 641 |
# ── Sweep button ──────────��───────────────────────────────────────────────
|
|
|
|
| 645 |
"🚀 Run Consensus Sweep",
|
| 646 |
type="primary",
|
| 647 |
use_container_width=True,
|
| 648 |
+
disabled=(is_training or sweep_complete),
|
| 649 |
+
help="Only runs years missing today's fresh results"
|
| 650 |
)
|
| 651 |
with col_info:
|
| 652 |
+
if sweep_complete:
|
| 653 |
+
st.success(f"✅ Today's sweep complete ({today_str}) — {len(SWEEP_YEARS)}/{len(SWEEP_YEARS)} years fresh")
|
| 654 |
elif is_training:
|
| 655 |
+
st.warning(f"⏳ Training in progress... ({len(sweep_cache)}/{len(SWEEP_YEARS)} fresh today)")
|
| 656 |
else:
|
| 657 |
+
st.info(
|
| 658 |
+
f"**{len(sweep_cache)}/{len(SWEEP_YEARS)}** years fresh for today ({today_str}). \n"
|
| 659 |
+
f"Will trigger **{len(years_needing_run)}** jobs: "
|
| 660 |
+
f"{', '.join(str(y) for y in years_needing_run)}"
|
| 661 |
+
)
|
| 662 |
|
| 663 |
+
if sweep_btn and years_needing_run:
|
| 664 |
+
sweep_mode_str = ",".join(str(y) for y in years_needing_run)
|
| 665 |
with st.spinner(f"🚀 Triggering parallel training for: {sweep_mode_str}..."):
|
| 666 |
ok = trigger_github_training(
|
| 667 |
+
start_year=years_needing_run[0],
|
| 668 |
sweep_mode=sweep_mode_str,
|
| 669 |
force_refresh=False
|
| 670 |
)
|
| 671 |
if ok:
|
| 672 |
st.success(
|
| 673 |
+
f"✅ Triggered **{len(years_needing_run)}** parallel jobs for: {sweep_mode_str}. "
|
| 674 |
f"Each takes ~90 mins. Refresh this tab when complete."
|
| 675 |
)
|
| 676 |
time.sleep(2)
|
|
|
|
| 679 |
st.error("❌ Failed to trigger GitHub Actions sweep.")
|
| 680 |
|
| 681 |
# ── Consensus results ─────────────────────────────────────────────────────
|
| 682 |
+
if len(display_cache) == 0:
|
| 683 |
st.info("👆 Click **🚀 Run Consensus Sweep** to train all years.")
|
| 684 |
st.stop()
|
| 685 |
|
| 686 |
+
consensus = compute_consensus(display_cache)
|
| 687 |
if not consensus:
|
| 688 |
st.warning("⚠️ Could not compute consensus.")
|
| 689 |
st.stop()
|
|
|
|
| 692 |
w_info = consensus['etf_summary'][winner]
|
| 693 |
win_color = ETF_COLORS.get(winner, "#00d1b2")
|
| 694 |
score_share = w_info['score_share'] * 100
|
| 695 |
+
n_cached = len(display_cache)
|
| 696 |
|
| 697 |
# ── Consensus winner banner ───────────────────────────────────────────────
|
| 698 |
split_signal = w_info['score_share'] < 0.4
|
|
|
|
| 705 |
padding:32px;text-align:center;margin:20px 0;
|
| 706 |
box-shadow:0 8px 24px rgba(0,0,0,0.4);">
|
| 707 |
<div style="font-size:11px;letter-spacing:3px;color:#aaa;margin-bottom:12px;">
|
| 708 |
+
WEIGHTED CONSENSUS · TFT · {n_cached} START YEARS · {today_str}
|
| 709 |
</div>
|
| 710 |
<div style="font-size:72px;font-weight:900;color:{win_color};
|
| 711 |
text-shadow:0 0 30px {win_color}88;letter-spacing:2px;">
|
|
|
|
| 819 |
for row in sorted(consensus['per_year'], key=lambda r: r['year']):
|
| 820 |
etf = row['signal']
|
| 821 |
col = ETF_COLORS.get(etf, "#888")
|
| 822 |
+
_in_today = row['year'] in sweep_cache
|
| 823 |
table_rows.append({
|
| 824 |
'Start Year': row['year'],
|
| 825 |
'Signal': etf,
|
|
|
|
| 830 |
'Sharpe': f"{row['sharpe']:.2f}",
|
| 831 |
'Max Drawdown': f"{row['max_dd']*100:.2f}%",
|
| 832 |
'Lookback': f"{row['lookback']}d",
|
| 833 |
+
'Cache': "✅ Today" if row['year'] in sweep_cache else "📅 Prev",
|
| 834 |
})
|
| 835 |
|
| 836 |
tbl_df = pd.DataFrame(table_rows)
|