import streamlit as st
import plotly.graph_objects as go
from datetime import datetime, timedelta
import pytz
from trading.journal import (
create_trades_table, get_open_trades, get_trade_history,
add_trade, update_trade, delete_trade, TradeEntry,
get_open_trades_summary, get_current_prices, generate_position_id,
get_position_summary, get_latest_portfolio_value, update_portfolio_value
)
def calculate_weekly_metrics(trades) -> dict:
"""Calculate weekly performance metrics"""
pacific_tz = pytz.timezone('US/Pacific')
now = datetime.now(pacific_tz)
# Create week_start without timezone first, then localize it
week_start = (now - timedelta(days=now.weekday())).replace(
hour=0, minute=0, second=0, microsecond=0
).astimezone(pacific_tz)
weekly_pl = 0
weekly_trades = []
for trade in trades:
# Get the trade date and ensure it's timezone aware
trade_date = trade.get('exit_date', trade.get('entry_date'))
if trade_date:
# Convert to Pacific timezone if needed
if trade_date.tzinfo is None:
trade_date = pacific_tz.localize(trade_date)
else:
trade_date = trade_date.astimezone(pacific_tz)
# For sells/exits that happened this week
if (trade.get('direction') == 'sell' or trade.get('exit_price')) and trade_date and trade_date >= week_start:
price = float(trade.get('exit_price') or trade.get('entry_price'))
shares = float(trade['shares'])
position_id = trade['position_id']
# Find matching buy order
buy_trades = [t for t in trades
if t['position_id'] == position_id and
(t.get('direction', '').lower() != 'sell' and not t.get('exit_price'))]
if buy_trades:
# Calculate average entry price
total_cost = sum(float(t['entry_price']) * float(t['shares']) for t in buy_trades)
total_shares = sum(float(t['shares']) for t in buy_trades)
avg_entry_price = total_cost / total_shares if total_shares > 0 else 0
# Calculate trade P/L
trade_pl = (price - avg_entry_price) * shares
weekly_pl += trade_pl
weekly_trades.append(trade)
return {
'weekly_pl': weekly_pl,
'weekly_trade_count': len(weekly_trades)
}
def calculate_portfolio_metrics(position_value: float, current_value: float, total_portfolio_value: float) -> dict:
"""Calculate portfolio metrics for a position"""
allocation = (current_value / total_portfolio_value) * 100 if total_portfolio_value > 0 else 0
position_pl = current_value - position_value
pl_impact = (position_pl / total_portfolio_value) * 100 if total_portfolio_value > 0 else 0
return {
'allocation': allocation,
'position_pl': position_pl,
'pl_impact': pl_impact
}
def calculate_position_performance(trades):
"""Calculate performance metrics for a group of trades"""
total_bought = 0
total_cost = 0
total_sold = 0
total_proceeds = 0
for trade in trades:
try:
shares = float(trade['shares'])
# Handle buy orders
if ('direction' not in trade or not trade['direction'] or
trade.get('direction', '').lower() == 'buy'):
if trade.get('entry_price'):
price = float(trade['entry_price'])
total_bought += shares
total_cost += shares * price
# Handle sell orders
elif trade.get('direction', '').lower() == 'sell' or trade.get('exit_price'):
price = float(trade.get('exit_price') or trade.get('entry_price'))
total_sold += shares
total_proceeds += shares * price
print(f"Processing trade: Direction={trade.get('direction')}, "
f"Shares={shares}, Price={price}") # Debug
except (ValueError, TypeError) as e:
print(f"Error processing trade: {e}")
continue
print(f"Performance calculation results:") # Debug
print(f"Total bought: {total_bought} shares at total cost: ${total_cost}") # Debug
print(f"Total sold: {total_sold} shares with total proceeds: ${total_proceeds}") # Debug
# Avoid division by zero
if total_bought == 0:
return {
'total_bought': 0,
'total_sold': 0,
'avg_entry': 0,
'avg_exit': 0,
'realized_pl': 0,
'remaining_shares': 0
}
avg_entry = total_cost / total_bought if total_bought > 0 else 0
avg_exit = total_proceeds / total_sold if total_sold > 0 else 0
realized_pl = total_proceeds - (total_sold / total_bought * total_cost) if total_sold > 0 else 0
remaining_shares = total_bought - total_sold
return {
'total_bought': total_bought,
'total_sold': total_sold,
'avg_entry': avg_entry,
'avg_exit': avg_exit,
'realized_pl': realized_pl,
'remaining_shares': remaining_shares
}
def format_datetime(dt):
"""Format datetime for display"""
if dt:
return dt.strftime('%Y-%m-%d %H:%M')
return ''
def plot_trade_history(trades):
"""Create a P/L chart using Plotly showing only realized profits/losses"""
if not trades:
return None
# Prepare data
dates = []
pnl = []
cumulative_pnl = 0
for trade in trades:
try:
# For sell orders
if trade.get('direction') == 'sell' or trade.get('exit_price'):
price = float(trade.get('exit_price') or trade.get('entry_price'))
date = trade.get('exit_date') or trade.get('entry_date')
shares = float(trade['shares'])
# Find matching buy order to calculate P/L
position_id = trade['position_id']
buy_trades = [t for t in trades
if t['position_id'] == position_id and
(t.get('direction', '').lower() != 'sell' and not t.get('exit_price'))]
if buy_trades:
# Use average entry price from buy trades
total_cost = sum(float(t['entry_price']) * float(t['shares']) for t in buy_trades)
total_shares = sum(float(t['shares']) for t in buy_trades)
avg_entry_price = total_cost / total_shares if total_shares > 0 else 0
# Calculate P/L
trade_pnl = (price - avg_entry_price) * shares
cumulative_pnl += trade_pnl
dates.append(date)
pnl.append(cumulative_pnl)
except (ValueError, TypeError) as e:
print(f"Error processing trade for P/L chart: {e}")
continue
if not dates:
return None
# Create figure
fig = go.Figure()
fig.add_trace(
go.Scatter(x=dates, y=pnl, mode='lines+markers',
name='Realized P/L',
line=dict(color='blue'),
hovertemplate='Date: %{x}
Realized P/L: $%{y:.2f}')
)
fig.update_layout(
title='Cumulative Realized Profit/Loss Over Time',
xaxis_title='Date',
yaxis_title='Realized P/L ($)',
hovermode='x unified'
)
return fig
def trading_journal_page():
st.header("Trading Journal")
print("\n=== Starting Trading Journal Page ===") # Debug
# Tabs for different journal functions
tab1, tab2, tab3, tab4 = st.tabs(["Open Positions", "Add Trade", "Update Trade", "Trade History"])
with tab1:
st.subheader("Open Positions")
print("\n--- Fetching Open Positions ---") # Debug
open_trades = get_open_trades()
print(f"Retrieved {len(open_trades) if open_trades else 0} open trades") # Debug
print(f"Open trades {open_trades}") # Debug
open_summary = get_open_trades_summary()
print(f"Retrieved {len(open_summary) if open_summary else 0} position summaries") # Debug
print(f"Open summary {open_summary}") # Debug
if open_summary:
# Get current prices and latest portfolio value
unique_tickers = list(set(summary['ticker'] for summary in open_summary))
print(f"Fetching prices for tickers: {unique_tickers}") # Debug
current_prices = get_current_prices(unique_tickers)
print(f"Retrieved current prices: {current_prices}") # Debug
latest_portfolio = get_latest_portfolio_value()
total_portfolio_value = latest_portfolio['total_value'] if latest_portfolio else 0
cash_balance = latest_portfolio['cash_balance'] if latest_portfolio else 0
# Get trade history for weekly metrics
trade_history = get_trade_history()
weekly_metrics = calculate_weekly_metrics(trade_history) if trade_history else {'weekly_pl': 0, 'weekly_trade_count': 0}
# Portfolio overview section
st.subheader("Portfolio Overview")
col1, col2, col3 = st.columns(3)
# Get cash balance
latest_portfolio = get_latest_portfolio_value()
cash_balance = latest_portfolio['cash_balance'] if latest_portfolio else 0
# Calculate total invested value and paper P/L
total_invested_value = 0
total_paper_pl = 0
if open_summary:
for summary in open_summary:
ticker = summary['ticker']
current_price = current_prices.get(ticker, 0)
shares = float(summary['total_shares'])
avg_entry = float(summary['avg_entry_price'])
# Calculate current position value using current market price
position_value = current_price * shares
total_invested_value += position_value
total_paper_pl += (current_price - avg_entry) * shares
# Calculate total portfolio value (cash + invested value)
total_portfolio_value = cash_balance + total_invested_value
with col1:
# Add input for cash balance
new_cash_balance = st.number_input(
"Cash Balance ($)",
value=cash_balance,
step=100.0,
format="%.2f"
)
if new_cash_balance != cash_balance:
# Update cash balance if changed
update_portfolio_value(total_portfolio_value, new_cash_balance)
st.rerun()
st.metric("Total Portfolio Value",
f"${total_portfolio_value:,.2f}",
f"Cash: ${cash_balance:,.2f} | Positions: ${total_invested_value:,.2f}")
with col2:
# Weekly metrics
weekly_pl_pct = (weekly_metrics['weekly_pl'] / total_portfolio_value * 100) if total_portfolio_value > 0 else 0
st.metric("Week P/L",
f"${weekly_metrics['weekly_pl']:,.2f}",
f"{weekly_pl_pct:.2f}% | {weekly_metrics['weekly_trade_count']} trades")
with col3:
# Calculate realized P/L from closed trades
realized_pl = 0
if trade_history:
for trade in trade_history:
if trade.get('exit_price'): # Only count closed trades
entry_price = float(trade['entry_price'])
exit_price = float(trade['exit_price'])
shares = float(trade['shares'])
realized_pl += (exit_price - entry_price) * shares
# Calculate paper P/L from open positions
paper_pl = 0
if open_summary:
for summary in open_summary:
ticker = summary['ticker']
current_price = current_prices.get(ticker, 0)
shares = summary['total_shares']
avg_entry = summary['avg_entry_price']
paper_pl += (current_price - avg_entry) * shares
# Calculate overall P/L
overall_pl = realized_pl + paper_pl
overall_pl_pct = (overall_pl / total_portfolio_value * 100) if total_portfolio_value > 0 else 0
st.metric("Overall P/L",
f"${overall_pl:,.2f}",
f"{overall_pl_pct:.2f}% (R: ${realized_pl:,.2f} | U: ${paper_pl:,.2f})")
total_paper_pl = 0
invested_value = 0
for summary in open_summary:
print(f"\nProcessing summary for {summary['ticker']}") # Debug
with st.expander(f"{summary['ticker']} Summary"):
ticker = summary['ticker']
avg_entry = summary['avg_entry_price']
current_price = current_prices.get(ticker)
total_shares = summary['total_shares']
position_value = avg_entry * total_shares
print(f"Position details for {ticker}:") # Debug
print(f"- Shares: {total_shares}") # Debug
print(f"- Avg Entry: {avg_entry}") # Debug
print(f"- Current Price: {current_price}") # Debug
print(f"- Position Value: {position_value}") # Debug
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total Shares", f"{total_shares:,}")
st.metric("Average Entry", f"${avg_entry:.2f}")
st.metric("Position Value", f"${position_value:.2f}")
with col2:
if current_price:
current_value = current_price * total_shares
paper_pl = (current_price - avg_entry) * total_shares
pl_percentage = (paper_pl / position_value) * 100
metrics = calculate_portfolio_metrics(
position_value, current_value, total_portfolio_value
)
st.metric("Current Price", f"${current_price:.2f}")
st.metric("Paper P/L", f"${paper_pl:.2f}",
f"{pl_percentage:.2f}%")
total_paper_pl += paper_pl
invested_value += current_value
with col3:
if current_price:
st.metric("Portfolio Allocation",
f"{metrics['allocation']:.1f}%")
st.metric("P/L Impact on Portfolio",
f"{metrics['pl_impact']:.2f}%")
# Update portfolio summary section
if invested_value > 0:
st.markdown("---")
st.subheader("Portfolio Summary")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total Invested Value",
f"${invested_value:.2f}",
f"{(invested_value/total_portfolio_value)*100:.1f}% of Portfolio")
with col2:
st.metric("Total P/L",
f"${total_paper_pl:.2f}",
f"{(total_paper_pl/total_portfolio_value)*100:.2f}% of Portfolio")
with col3:
st.metric("Cash Allocation",
f"${cash_balance:.2f}",
f"{(cash_balance/total_portfolio_value)*100:.1f}% of Portfolio")
with tab2:
st.subheader("Add New Trade")
print("\n--- Add Trade Tab ---") # Debug
ticker = st.text_input("Ticker Symbol").upper()
print(f"Ticker entered: {ticker}") # Debug
# Add direction selection
direction = st.selectbox(
"Direction",
["Buy", "Sell"],
key="trade_direction"
)
print(f"Direction selected: {direction}") # Debug
if ticker:
print(f"\nProcessing ticker {ticker}") # Debug
existing_positions = get_position_summary(ticker)
print(f"Found existing positions: {existing_positions}") # Debug
# Handle position selection based on direction
if direction == "Buy":
if existing_positions:
print(f"Processing existing positions for {ticker}") # Debug
st.write(f"Existing {ticker} Positions:")
for pos in existing_positions:
print(f"Position details: {pos}") # Debug
st.write(f"Position ID: {pos['position_id']}")
st.write(f"Total Shares: {pos['total_shares']}")
st.write(f"Average Entry: ${pos['avg_entry_price']:.2f}")
add_to_existing = st.checkbox("Add to existing position")
print(f"Add to existing selected: {add_to_existing}") # Debug
if add_to_existing:
position_id = st.selectbox(
"Select Position ID",
options=[pos['position_id'] for pos in existing_positions],
key="position_select"
)
print(f"Selected position ID: {position_id}") # Debug
else:
position_id = None
print("Creating new position") # Debug
else:
position_id = None
print("No existing positions found, creating new position") # Debug
else: # Sell
if not existing_positions:
st.error("No existing positions found for this ticker")
st.stop()
position_id = st.selectbox(
"Select Position to Exit",
options=[pos['position_id'] for pos in existing_positions],
key="position_select"
)
# Get entry date/time
entry_date = st.date_input("Entry Date")
entry_time_str = st.text_input("Entry Time (HH:MM)", "09:30")
try:
entry_time = datetime.strptime(entry_time_str, "%H:%M").time()
entry_datetime = datetime.combine(entry_date, entry_time)
entry_datetime = pytz.timezone('US/Pacific').localize(entry_datetime)
except ValueError:
st.error("Please enter time in HH:MM format (e.g. 09:30)")
st.stop()
# Generate position_id if needed
if position_id is None:
position_id = generate_position_id(ticker, entry_datetime)
col1, col2 = st.columns(2)
with col1:
shares = st.number_input("Number of Shares", min_value=1, step=1)
if direction == "Buy":
entry_price = st.number_input("Entry Price", min_value=0.01, step=0.01)
else:
entry_price = st.number_input("Exit Price", min_value=0.01, step=0.01)
with col2:
if direction == "Buy":
target_price = st.number_input("Target Price", min_value=0.01, step=0.01)
stop_loss = st.number_input("Stop Loss", min_value=0.01, step=0.01)
strategy = st.text_input("Strategy")
else:
exit_reason = st.text_area("Exit Reason", key="exit_reason")
order_type = st.selectbox("Order Type", ["Market", "Limit"], key="add_trade_order_type")
if direction == "Buy":
followed_rules = st.checkbox("Followed Trading Rules")
entry_reason = st.text_area("Entry Reason", key="add_trade_reason")
notes = st.text_area("Notes", key="add_trade_notes")
if st.button("Add Trade"):
try:
print("\n--- Processing Add Trade ---") # Debug
entry_datetime = datetime.combine(entry_date, entry_time)
entry_datetime = pytz.timezone('US/Pacific').localize(entry_datetime)
print(f"Entry datetime: {entry_datetime}") # Debug
trade = TradeEntry(
ticker=ticker,
entry_date=entry_datetime,
shares=shares,
entry_price=entry_price,
target_price=target_price if direction == "Buy" else None,
stop_loss=stop_loss if direction == "Buy" else None,
strategy=strategy if direction == "Buy" else None,
order_type=order_type,
position_id=position_id,
followed_rules=followed_rules if direction == "Buy" else None,
entry_reason=entry_reason if direction == "Buy" else None,
exit_reason=exit_reason if direction == "Sell" else None,
notes=notes,
direction=direction.lower()
)
print(f"Trade object created: {trade.__dict__}") # Debug
add_trade(trade)
print("Trade added successfully") # Debug
st.success("Trade added successfully!")
st.query_params(rerun=True)
except Exception as e:
print(f"Error adding trade: {str(e)}") # Debug
st.error(f"Error adding trade: {str(e)}")
with tab3:
st.subheader("Update Trade")
print("\n--- Update Trade Tab ---") # Debug
open_trades = get_open_trades()
print(f"Retrieved {len(open_trades) if open_trades else 0} trades for update tab") # Debug
print(f"Update tab trades {open_trades}") # Debug
if open_trades:
trade_id = st.selectbox(
"Select Trade to Update",
options=[t['id'] for t in open_trades],
format_func=lambda x: f"{next(t['ticker'] for t in open_trades if t['id'] == x)} - {x}",
key="trade_select"
)
trade = next(t for t in open_trades if t['id'] == trade_id)
print(f"\nSelected trade for update: {trade}") # Debug
col1, col2 = st.columns(2)
with col1:
new_shares = st.number_input("Shares", value=trade['shares'] if trade['shares'] is not None else 0)
new_entry = st.number_input("Entry Price", value=float(trade['entry_price']) if trade['entry_price'] is not None else 0.0)
new_target = st.number_input("Target Price", value=float(trade['target_price']) if trade['target_price'] is not None else 0.0)
with col2:
new_stop = st.number_input("Stop Loss", value=float(trade['stop_loss']) if trade['stop_loss'] is not None else 0.0)
new_strategy = st.text_input("Strategy", value=trade['strategy'])
new_order_type = st.selectbox("Order Type", ["Market", "Limit"],
index=0 if trade['order_type'] == "Market" else 1,
key="update_trade_order_type")
# Add date and time fields
entry_date = st.date_input(
"Entry Date",
value=trade['entry_date'].date(),
key="update_entry_date"
)
entry_time_str = st.text_input(
"Entry Time (HH:MM)",
value=trade['entry_date'].strftime("%H:%M"),
key="update_entry_time"
)
try:
entry_time = datetime.strptime(entry_time_str, "%H:%M").time()
except ValueError:
st.error("Please enter time in HH:MM format (e.g. 09:30)")
st.stop()
new_notes = st.text_area("Notes",
value=trade['notes'] if trade['notes'] else "",
key="update_trade_notes")
if st.button("Update Trade"):
try:
# Combine date and time into datetime
entry_datetime = datetime.combine(entry_date, entry_time)
entry_datetime = pytz.timezone('US/Pacific').localize(entry_datetime)
updates = {
'entry_date': entry_datetime,
'shares': new_shares,
'entry_price': new_entry,
'target_price': new_target,
'stop_loss': new_stop,
'strategy': new_strategy,
'order_type': new_order_type,
'notes': new_notes
}
print("\n--- Processing Trade Update ---") # Debug
print(f"Update data being sent: {updates}") # Debug
update_trade(trade_id, updates)
print("Trade update completed") # Debug
st.success("Trade updated successfully!")
print("Clearing session state and triggering rerun") # Debug
# Clear session state
for key in st.session_state.keys():
del st.session_state[key]
st.rerun()
except Exception as e:
st.error(f"Error updating trade: {str(e)}")
else:
st.info("No open trades to update")
with tab4:
st.subheader("Trade History")
history = get_trade_history()
if history:
# Group trades by position_id
positions = {}
for trade in history:
if trade['position_id'] not in positions:
positions[trade['position_id']] = []
positions[trade['position_id']].append(trade)
# Add P/L chart
fig = plot_trade_history(history)
if fig:
st.plotly_chart(fig, use_container_width=True)
# Display trades grouped by position
for position_id, trades in positions.items():
# Sort trades chronologically
trades.sort(key=lambda x: x['entry_date'])
first_trade = trades[0]
# Calculate position performance
performance = calculate_position_performance(trades)
with st.expander(f"{first_trade['ticker']} - Position {position_id}"):
# Show position summary
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total Shares Bought", f"{int(performance['total_bought']):,}")
st.metric("Avg Entry", f"${performance['avg_entry']:.2f}")
with col2:
st.metric("Total Shares Sold", f"{int(performance['total_sold']):,}")
st.metric("Avg Exit", f"${performance['avg_exit']:.2f}")
with col3:
st.metric("Remaining Shares", f"{int(performance['remaining_shares']):,}")
st.metric("Realized P/L",
f"${performance['realized_pl']:.2f}",
f"{(performance['realized_pl'] / (performance['total_sold'] * performance['avg_entry']) * 100):.2f}%" if performance['total_sold'] > 0 else None)
st.markdown("---")
# Show buy trades
st.subheader("Buy Orders")
for trade in trades:
# Infer direction if not set
if 'direction' not in trade or not trade['direction']:
is_buy = trade.get('exit_price') is None
else:
is_buy = trade.get('direction', '').lower() == 'buy'
if is_buy:
col1, col2, col3 = st.columns(3)
with col1:
st.text(f"Date: {format_datetime(trade['entry_date'])}")
st.text(f"Shares: {trade['shares']}")
with col2:
st.text(f"Price: ${float(trade['entry_price']):.2f}")
st.text(f"Order: {trade['order_type']}")
with col3:
if trade.get('target_price'):
st.text(f"Target: ${float(trade['target_price']):.2f}")
if trade.get('stop_loss'):
st.text(f"Stop: ${float(trade['stop_loss']):.2f}")
if trade.get('entry_reason'):
st.text(f"Reason: {trade['entry_reason']}")
st.markdown("---")
# Show sell trades
st.subheader("Sell Orders")
for trade in trades:
# Check for sell orders
is_sell = (trade.get('direction', '').lower() == 'sell' or
trade.get('exit_price') is not None)
if is_sell:
col1, col2 = st.columns(2)
with col1:
st.text(f"Date: {format_datetime(trade.get('exit_date') or trade.get('entry_date'))}")
st.text(f"Shares: {trade['shares']}")
with col2:
# Use exit_price for sells, fallback to entry_price
price = float(trade.get('exit_price') or trade.get('entry_price'))
st.text(f"Price: ${price:.2f}")
st.text(f"Order: {trade['order_type']}")
if trade.get('exit_reason'):
st.text(f"Reason: {trade['exit_reason']}")
st.markdown("---")
else:
st.info("No trade history found")