183 lines
4.5 KiB
Python
183 lines
4.5 KiB
Python
# app/streamlit_app.py
|
|
import streamlit as st
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import pandas as pd
|
|
from history import load_monthly_close
|
|
from market import us_market_status
|
|
from data import fetch_live_price
|
|
from strategy import allocation
|
|
from execution import try_execute_sip
|
|
from state import load_state, save_state
|
|
from mtm import log_mtm, should_log_mtm
|
|
|
|
if "engine_active" not in st.session_state:
|
|
st.session_state.engine_active = False
|
|
|
|
SP500 = "SPY"
|
|
GOLD = "GLD"
|
|
GOLD_FALLBACK = "IAU"
|
|
SMA_MONTHS = 36
|
|
|
|
def get_prices():
|
|
try:
|
|
sp = fetch_live_price("SPY")
|
|
except Exception as e:
|
|
st.error(f"SPY price error: {e}")
|
|
return None, None
|
|
|
|
try:
|
|
gd = fetch_live_price("GLD")
|
|
except Exception:
|
|
try:
|
|
gd = fetch_live_price("IAU")
|
|
except Exception as e:
|
|
st.error(f"Gold price error: {e}")
|
|
return None, None
|
|
|
|
return sp, gd
|
|
|
|
SIP_AMOUNT = st.number_input("SIP Amount ($)", 100, 5000, 1000)
|
|
SIP_INTERVAL_SEC = st.number_input("SIP Interval (sec) [TEST]", 30, 3600, 120)
|
|
|
|
st.title("SIPXAR — Phase-1 Safe Engine")
|
|
|
|
market_open, market_time = us_market_status()
|
|
st.info(f"Market {'OPEN' if market_open else 'CLOSED'} | ET {market_time}")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
if st.button("START ENGINE"):
|
|
st.session_state.engine_active = True
|
|
|
|
# HARD RESET
|
|
for f in [
|
|
Path("storage/state.json"),
|
|
Path("storage/mtm_ledger.csv"),
|
|
Path("storage/ledger.csv"),
|
|
]:
|
|
if f.exists():
|
|
f.unlink()
|
|
|
|
fresh_state = {
|
|
"total_invested": 0.0,
|
|
"sp_units": 0.0,
|
|
"gd_units": 0.0,
|
|
"last_sip_ts": datetime.utcnow().isoformat(),
|
|
}
|
|
save_state(fresh_state)
|
|
|
|
st.success("Engine started")
|
|
|
|
with col2:
|
|
if st.button("KILL ENGINE"):
|
|
st.session_state.engine_active = False
|
|
|
|
# HARD RESET
|
|
from pathlib import Path
|
|
|
|
for f in [
|
|
Path("storage/state.json"),
|
|
Path("storage/mtm_ledger.csv"),
|
|
Path("storage/ledger.csv"),
|
|
]:
|
|
if f.exists():
|
|
f.unlink()
|
|
|
|
st.session_state.clear()
|
|
st.warning("Engine killed and state wiped")
|
|
|
|
st.stop()
|
|
|
|
if not st.session_state.engine_active:
|
|
st.info("Engine is stopped. Click START to begin.")
|
|
st.stop()
|
|
|
|
state = load_state()
|
|
|
|
sp_price, gd_price = get_prices()
|
|
|
|
if sp_price is None or gd_price is None:
|
|
st.stop()
|
|
|
|
st.write("Engine Prices")
|
|
st.write("SP Price:", sp_price)
|
|
st.write("Gold Price:", gd_price)
|
|
|
|
sp_hist = load_monthly_close(SP500)
|
|
gd_hist = load_monthly_close(GOLD_FALLBACK)
|
|
|
|
sp_sma = sp_hist.rolling(SMA_MONTHS).mean().iloc[-1]
|
|
gd_sma = gd_hist.rolling(SMA_MONTHS).mean().iloc[-1]
|
|
|
|
eq_w, gd_w = allocation(
|
|
sp_price=sp_price,
|
|
gd_price=gd_price,
|
|
sp_sma=sp_sma,
|
|
gd_sma=gd_sma
|
|
)
|
|
|
|
state, executed = try_execute_sip(
|
|
now=datetime.utcnow(),
|
|
market_open=market_open,
|
|
sip_interval=SIP_INTERVAL_SEC,
|
|
sip_amount=SIP_AMOUNT,
|
|
sp_price=sp_price,
|
|
gd_price=gd_price,
|
|
eq_w=eq_w,
|
|
gd_w=gd_w
|
|
)
|
|
|
|
MTM_FILE = Path("storage/mtm_ledger.csv")
|
|
|
|
if MTM_FILE.exists():
|
|
mtm_df = pd.read_csv(MTM_FILE)
|
|
else:
|
|
mtm_df = pd.DataFrame()
|
|
|
|
now = datetime.utcnow()
|
|
|
|
if market_open and should_log_mtm(mtm_df, now):
|
|
portfolio_value, pnl = log_mtm(
|
|
sp_units=state["sp_units"],
|
|
gd_units=state["gd_units"],
|
|
sp_price=sp_price,
|
|
gd_price=gd_price,
|
|
total_invested=state["total_invested"],
|
|
)
|
|
else:
|
|
# Do NOT log MTM when market is closed
|
|
portfolio_value = (
|
|
state["sp_units"] * sp_price +
|
|
state["gd_units"] * gd_price
|
|
)
|
|
pnl = portfolio_value - state["total_invested"]
|
|
|
|
st.metric("Total Invested", f"${state['total_invested']:,.2f}")
|
|
st.metric("SP Units", round(state["sp_units"], 4))
|
|
st.metric("Gold Units", round(state["gd_units"], 4))
|
|
st.metric("Portfolio Value", f"${portfolio_value:,.2f}")
|
|
st.metric("Unrealized PnL", f"${pnl:,.2f}", delta=f"{pnl:,.2f}")
|
|
|
|
st.subheader("Equity Curve (Unrealized PnL)")
|
|
|
|
if not mtm_df.empty and len(mtm_df) > 1:
|
|
mtm_df["timestamp"] = pd.to_datetime(mtm_df["timestamp"])
|
|
mtm_df = mtm_df.sort_values("timestamp")
|
|
mtm_df.set_index("timestamp", inplace=True)
|
|
|
|
st.line_chart(
|
|
mtm_df["pnl"],
|
|
height=350,
|
|
)
|
|
else:
|
|
st.info("Equity curve will appear after sufficient MTM data.")
|
|
|
|
if executed:
|
|
st.success("SIP Executed")
|
|
|
|
time.sleep(5)
|
|
st.rerun()
|