181 lines
7.3 KiB
Python
181 lines
7.3 KiB
Python
"""
|
|
Portfolio page — position tracker with P/L visualization.
|
|
|
|
Shows portfolio summary KPIs and individual position rows
|
|
with color-coded P/L indicators.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
import pandas as pd
|
|
import streamlit as st
|
|
|
|
from tradingagents.ui.theme import COLORS, kpi_card, page_header, pnl_color
|
|
from tradingagents.ui.utils import load_open_positions
|
|
|
|
|
|
def render():
|
|
st.markdown(page_header("Portfolio", "Open positions & P/L tracker"), unsafe_allow_html=True)
|
|
|
|
# ---- Add position form ----
|
|
with st.expander("Add Position"):
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
with col1:
|
|
ticker = st.text_input("Ticker", placeholder="AAPL")
|
|
with col2:
|
|
entry_price = st.number_input("Entry Price", min_value=0.0, format="%.2f")
|
|
with col3:
|
|
shares = st.number_input("Shares", min_value=0, step=1)
|
|
with col4:
|
|
st.write("")
|
|
if st.button("Add Position"):
|
|
if ticker and entry_price > 0 and shares > 0:
|
|
from tradingagents.dataflows.discovery.performance.position_tracker import (
|
|
PositionTracker,
|
|
)
|
|
|
|
tracker = PositionTracker()
|
|
pos = tracker.create_position(
|
|
{
|
|
"ticker": ticker.upper(),
|
|
"entry_price": entry_price,
|
|
"shares": shares,
|
|
"recommendation_date": datetime.now().isoformat(),
|
|
"pipeline": "manual",
|
|
"scanner": "manual",
|
|
"strategy_match": "manual",
|
|
"confidence": 5,
|
|
}
|
|
)
|
|
tracker.save_position(pos)
|
|
st.success(f"Added {ticker.upper()}")
|
|
st.rerun()
|
|
|
|
positions = load_open_positions()
|
|
|
|
if not positions:
|
|
st.markdown(
|
|
f"""
|
|
<div style="text-align:center;padding:3rem;color:{COLORS['text_muted']};
|
|
font-family:'DM Sans',sans-serif;">
|
|
<div style="font-size:2rem;margin-bottom:0.5rem;">No open positions</div>
|
|
<div style="font-size:0.85rem;">
|
|
Enter positions manually above or run the discovery pipeline.
|
|
</div>
|
|
</div>
|
|
""",
|
|
unsafe_allow_html=True,
|
|
)
|
|
return
|
|
|
|
# ---- Portfolio summary ----
|
|
total_invested = sum(p["entry_price"] * p.get("shares", 0) for p in positions)
|
|
total_current = sum(p["metrics"]["current_price"] * p.get("shares", 0) for p in positions)
|
|
total_pnl = total_current - total_invested
|
|
total_pnl_pct = (total_pnl / total_invested * 100) if total_invested > 0 else 0
|
|
|
|
cols = st.columns(4)
|
|
summary_kpis = [
|
|
("Invested", f"${total_invested:,.0f}", "", "blue"),
|
|
("Current Value", f"${total_current:,.0f}", "", "blue"),
|
|
(
|
|
"P/L",
|
|
f"${total_pnl:+,.0f}",
|
|
f"{total_pnl_pct:+.1f}%",
|
|
"green" if total_pnl >= 0 else "red",
|
|
),
|
|
("Positions", str(len(positions)), "", "amber"),
|
|
]
|
|
for col, (label, value, delta, color) in zip(cols, summary_kpis):
|
|
with col:
|
|
st.markdown(kpi_card(label, value, delta, color), unsafe_allow_html=True)
|
|
|
|
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
|
|
|
|
# ---- Position cards ----
|
|
st.markdown(
|
|
'<div class="section-title">Open Positions <span class="accent">// live</span></div>',
|
|
unsafe_allow_html=True,
|
|
)
|
|
|
|
for p in positions:
|
|
ticker = p["ticker"]
|
|
entry = p["entry_price"]
|
|
current = p["metrics"]["current_price"]
|
|
shares = p.get("shares", 0)
|
|
pnl_dollar = (current - entry) * shares
|
|
pnl_pct = p["metrics"]["current_return"]
|
|
days = p["metrics"]["days_held"]
|
|
color = pnl_color(pnl_pct)
|
|
|
|
st.markdown(
|
|
f"""
|
|
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
|
|
border-left:3px solid {color};border-radius:8px;
|
|
padding:0.85rem 1.1rem;margin-bottom:0.5rem;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
|
<div style="display:flex;align-items:center;gap:1rem;">
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-weight:700;font-size:1.05rem;
|
|
color:{COLORS['text_primary']};">{ticker}</span>
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-size:0.72rem;color:{COLORS['text_muted']};">
|
|
{shares} shares · {days}d
|
|
</span>
|
|
</div>
|
|
<div style="text-align:right;">
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-size:1rem;font-weight:700;color:{color};">
|
|
{pnl_pct:+.1f}%
|
|
</span>
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-size:0.75rem;color:{COLORS['text_muted']};margin-left:0.5rem;">
|
|
${pnl_dollar:+,.0f}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:2rem;margin-top:0.4rem;">
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-size:0.72rem;color:{COLORS['text_muted']};">
|
|
Entry <span style="color:{COLORS['text_secondary']};">${entry:.2f}</span>
|
|
</span>
|
|
<span style="font-family:'JetBrains Mono',monospace;
|
|
font-size:0.72rem;color:{COLORS['text_muted']};">
|
|
Current <span style="color:{COLORS['text_secondary']};">${current:.2f}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
""",
|
|
unsafe_allow_html=True,
|
|
)
|
|
|
|
# ---- Data table fallback ----
|
|
with st.expander("Detailed Table"):
|
|
data = []
|
|
for p in positions:
|
|
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0)
|
|
data.append(
|
|
{
|
|
"Ticker": p["ticker"],
|
|
"Entry": p["entry_price"],
|
|
"Current": p["metrics"]["current_price"],
|
|
"Shares": p.get("shares", 0),
|
|
"P/L": pnl,
|
|
"P/L %": p["metrics"]["current_return"],
|
|
"Days": p["metrics"]["days_held"],
|
|
}
|
|
)
|
|
st.dataframe(
|
|
pd.DataFrame(data),
|
|
width="stretch",
|
|
hide_index=True,
|
|
column_config={
|
|
"Entry": st.column_config.NumberColumn(format="$%.2f"),
|
|
"Current": st.column_config.NumberColumn(format="$%.2f"),
|
|
"Shares": st.column_config.NumberColumn(format="%d"),
|
|
"P/L": st.column_config.NumberColumn(format="$%+.2f"),
|
|
"P/L %": st.column_config.NumberColumn(format="%+.1f%%"),
|
|
"Days": st.column_config.NumberColumn(format="%d"),
|
|
},
|
|
)
|