SIP_GoldBees_Backend/strategy_code/US_paper_trading_yfinance.py
2026-02-01 13:13:41 +00:00

256 lines
6.8 KiB
Python

import streamlit as st
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta, time as dtime
import pytz
import time
# =========================
# CONFIG
# =========================
SP500 = "SPY"
GOLD = "GLD"
SMA_MONTHS = 36
BASE_EQUITY = 0.60
TILT_MULT = 1.5
MAX_TILT = 0.25
MIN_EQUITY = 0.20
MAX_EQUITY = 0.90
PRICE_REFRESH_SEC = 5
# =========================
# DATA
# =========================
@st.cache_data(ttl=3600)
def load_history(ticker):
df = yf.download(
ticker,
period="10y",
auto_adjust=True,
progress=False,
)
if df.empty:
raise RuntimeError(f"No historical data for {ticker}")
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
if "Close" not in df.columns:
raise RuntimeError(f"'Close' not found for {ticker}")
series = df["Close"].copy()
if not isinstance(series.index, pd.DatetimeIndex):
raise RuntimeError(f"Index is not DatetimeIndex for {ticker}")
series = series.resample("M").last()
return series
@st.cache_data(ttl=15)
def live_price(ticker):
df = yf.download(
ticker,
period="1d",
interval="1m",
progress=False,
)
if df.empty:
raise RuntimeError(f"No live data for {ticker}")
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
price = df["Close"].iloc[-1]
return float(price)
def us_market_status():
tz = pytz.timezone("America/New_York")
now = datetime.now(tz)
market_open = dtime(9, 30)
market_close = dtime(16, 0)
is_weekday = now.weekday() < 5
is_open = is_weekday and market_open <= now.time() <= market_close
return {
"is_open": is_open,
"now_et": now.strftime("%Y-%m-%d %H:%M:%S"),
"session": "OPEN" if is_open else "CLOSED",
}
sp_hist = load_history(SP500)
gd_hist = load_history(GOLD)
hist_prices = pd.concat(
[sp_hist, gd_hist],
axis=1,
keys=["SP500", "GOLD"],
).dropna()
sma_sp = hist_prices["SP500"].rolling(SMA_MONTHS).mean()
sma_gd = hist_prices["GOLD"].rolling(SMA_MONTHS).mean()
# =========================
# STATE INIT
# =========================
if "running" not in st.session_state:
st.session_state.running = False
st.session_state.last_sip = None
st.session_state.total_invested = 0.0
st.session_state.sp_units = 0.0
st.session_state.gd_units = 0.0
st.session_state.last_sp_price = None
st.session_state.last_gd_price = None
if "pnl_ledger" not in st.session_state:
st.session_state.pnl_ledger = pd.DataFrame(
columns=["timestamp", "pnl"]
)
# =========================
# UI
# =========================
st.title("SIPXAR - Live SIP Portfolio (US)")
market = us_market_status()
if market["is_open"]:
st.success(f"US Market OPEN (ET {market['now_et']})")
else:
st.warning(f"US Market CLOSED (ET {market['now_et']})")
sip_amount = st.number_input("SIP Amount ($)", min_value=10, value=1000, step=100)
sip_minutes = st.number_input(
"SIP Frequency (minutes) - TEST MODE",
min_value=1,
value=2,
step=1,
)
sip_interval = timedelta(minutes=sip_minutes)
col1, col2 = st.columns(2)
if col1.button("START"):
st.session_state.running = True
if st.session_state.last_sip is None:
st.session_state.last_sip = datetime.utcnow() - sip_interval
if col2.button("STOP"):
st.session_state.running = False
# =========================
# ENGINE LOOP
# =========================
if st.session_state.running:
now = datetime.utcnow()
if market["is_open"]:
sp_price = live_price(SP500)
gd_price = live_price(GOLD)
st.session_state.last_sp_price = sp_price
st.session_state.last_gd_price = gd_price
else:
sp_price = st.session_state.get("last_sp_price")
gd_price = st.session_state.get("last_gd_price")
if sp_price is None or pd.isna(sp_price):
sp_price = hist_prices["SP500"].iloc[-1]
if gd_price is None or pd.isna(gd_price):
gd_price = hist_prices["GOLD"].iloc[-1]
if st.session_state.last_sip is None:
st.session_state.last_sip = now - sip_interval
# SIP trigger only when market is open
if (
market["is_open"]
and (now - st.session_state.last_sip) >= sip_interval
):
rd = (
(hist_prices["SP500"].iloc[-1] / sma_sp.iloc[-1])
- (hist_prices["GOLD"].iloc[-1] / sma_gd.iloc[-1])
)
tilt = np.clip(-rd * TILT_MULT, -MAX_TILT, MAX_TILT)
eq_w = np.clip(BASE_EQUITY * (1 + tilt), MIN_EQUITY, MAX_EQUITY)
gd_w = 1 - eq_w
sp_buy = sip_amount * eq_w
gd_buy = sip_amount * gd_w
st.session_state.sp_units += sp_buy / sp_price
st.session_state.gd_units += gd_buy / gd_price
st.session_state.total_invested += sip_amount
st.session_state.last_sip = now
# MTM
sp_val = st.session_state.sp_units * sp_price
gd_val = st.session_state.gd_units * gd_price
port_val = sp_val + gd_val
pnl = port_val - st.session_state.total_invested
st.session_state.pnl_ledger = pd.concat(
[
st.session_state.pnl_ledger,
pd.DataFrame(
{
"timestamp": [datetime.utcnow()],
"pnl": [pnl],
}
),
],
ignore_index=True,
)
# =========================
# DISPLAY
# =========================
st.subheader("Portfolio Snapshot")
st.caption(
"Prices updating live" if market["is_open"] else "Prices frozen - market closed"
)
c1, c2, c3 = st.columns(3)
c1.metric("Total Invested", f"${st.session_state.total_invested:,.2f}")
c2.metric("Portfolio Value", f"${port_val:,.2f}")
c3.metric("Unrealized PnL", f"${pnl:,.2f}", delta=f"{pnl:,.2f}")
next_sip_in = sip_interval - (now - st.session_state.last_sip)
next_sip_sec = max(0, int(next_sip_in.total_seconds()))
st.caption(f"Next SIP in ~ {next_sip_sec} seconds (TEST MODE)")
st.subheader("Equity Curve (PnL)")
if len(st.session_state.pnl_ledger) > 1:
pnl_df = st.session_state.pnl_ledger.copy()
pnl_df["timestamp"] = pd.to_datetime(pnl_df["timestamp"])
pnl_df.set_index("timestamp", inplace=True)
st.line_chart(
pnl_df["pnl"],
height=350,
)
else:
st.info("Equity curve will appear after portfolio updates.")
st.subheader("Holdings")
st.dataframe(
pd.DataFrame(
{
"Asset": ["SP500 (SPY)", "Gold (GLD)"],
"Units": [st.session_state.sp_units, st.session_state.gd_units],
"Price": [sp_price, gd_price],
"Value": [sp_val, gd_val],
}
)
)
time.sleep(PRICE_REFRESH_SEC)
st.rerun()