stock_system/src/trading/journal.py

901 lines
37 KiB
Python

from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import Optional
import pytz
from zoneinfo import ZoneInfo
import yfinance as yf
from db.db_connection import create_client
from trading.position_calculator import PositionCalculator
from utils.data_utils import get_user_input
def handle_sell_order(ticker: str, shares_to_sell: int, exit_price: float, exit_date: datetime,
order_type: str, followed_rules: bool, exit_reason: str, notes: Optional[str] = None) -> bool:
"""
Handle sell order using FIFO logic
Args:
ticker (str): Stock ticker
shares_to_sell (int): Number of shares to sell
exit_price (float): Exit price per share
exit_date (datetime): Exit date and time
order_type (str): Order type (Market/Limit)
followed_rules (bool): Whether trading rules were followed
exit_reason (str): Reason for exit
notes (Optional[str]): Additional notes
Returns:
bool: True if sell order was processed successfully
"""
with create_client() as client:
# Get open positions for this ticker ordered by entry date (FIFO)
query = f"""
SELECT id, shares, entry_price, position_id
FROM stock_db.trades
WHERE ticker = '{ticker}'
AND exit_price IS NULL
ORDER BY entry_date ASC
"""
result = client.query(query).result_rows
if not result:
print(f"No open positions found for {ticker}")
return False
remaining_shares = shares_to_sell
positions = [dict(zip(['id', 'shares', 'entry_price', 'position_id'], row)) for row in result]
total_available_shares = sum(pos['shares'] for pos in positions)
if shares_to_sell > total_available_shares:
print(f"Error: Attempting to sell {shares_to_sell} shares but only {total_available_shares} available")
return False
for position in positions:
if remaining_shares <= 0:
break
shares_from_position = min(remaining_shares, position['shares'])
if shares_from_position == position['shares']:
# Close entire position
update_trade(
trade_id=position['id'],
updates={
'exit_price': exit_price,
'exit_date': exit_date,
'followed_rules': 1 if followed_rules else 0,
'exit_reason': exit_reason,
'notes': notes
}
)
else:
# Split position: update original position with remaining shares
# and create a new closed position for the sold shares
new_position_shares = position['shares'] - shares_from_position
# Update original position with reduced shares
client.command(f"""
ALTER TABLE stock_db.trades
UPDATE shares = {new_position_shares}
WHERE id = {position['id']}
""")
# Create new record for the sold portion
trade = TradeEntry(
ticker=ticker,
entry_date=datetime.now(), # Use original entry date?
shares=shares_from_position,
entry_price=position['entry_price'],
target_price=0, # Not relevant for closed portion
stop_loss=0, # Not relevant for closed portion
strategy="FIFO_SPLIT",
order_type=order_type,
position_id=position['position_id'],
followed_rules=followed_rules,
exit_price=exit_price,
exit_date=exit_date,
exit_reason=exit_reason,
notes=notes
)
add_trade(trade)
remaining_shares -= shares_from_position
return True
def create_portfolio_table():
with create_client() as client:
query = """
CREATE TABLE IF NOT EXISTS stock_db.portfolio_history (
id UInt32,
date DateTime,
total_value Float64,
cash_balance Float64,
notes Nullable(String),
created_at DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (date, id)
"""
client.command(query)
def update_portfolio_value(total_value: float, cash_balance: float, notes: Optional[str] = None):
with create_client() as client:
query = f"""
INSERT INTO stock_db.portfolio_history (
id, date, total_value, cash_balance, notes
) VALUES (
{generate_id()},
'{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}',
{total_value},
{cash_balance},
{f"'{notes}'" if notes else 'NULL'}
)
"""
client.command(query)
def get_latest_portfolio_value() -> Optional[dict]:
with create_client() as client:
query = """
SELECT total_value, cash_balance, date
FROM stock_db.portfolio_history
ORDER BY date DESC
LIMIT 1
"""
result = client.query(query).result_rows
if result:
return {
'total_value': result[0][0],
'cash_balance': result[0][1],
'date': result[0][2]
}
return None
@dataclass
class TradeEntry:
ticker: str
entry_date: datetime
shares: int
entry_price: float
target_price: Optional[float] # Made optional since sell orders don't need it
stop_loss: Optional[float] # Made optional since sell orders don't need it
strategy: Optional[str] # Made optional since sell orders might not need it
order_type: str # Market/Limit
direction: str # 'buy' or 'sell'
followed_rules: Optional[bool] = None
entry_reason: Optional[str] = None
exit_price: Optional[float] = None
exit_date: Optional[datetime] = None
exit_reason: Optional[str] = None
notes: Optional[str] = None
position_id: Optional[str] = None
@property
def expected_profit_loss(self) -> Optional[float]:
if self.direction == 'buy' and self.target_price:
return (self.target_price - self.entry_price) * self.shares
return None
@property
def max_loss(self) -> Optional[float]:
if self.direction == 'buy' and self.stop_loss:
return (self.stop_loss - self.entry_price) * self.shares
return None
@property
def actual_profit_loss(self) -> Optional[float]:
if self.exit_price:
return (self.exit_price - self.entry_price) * self.shares
return None
def get_market_hours(date: datetime) -> tuple:
"""Get market open/close times in Eastern for given date"""
eastern = pytz.timezone('US/Eastern')
date_eastern = date.astimezone(eastern)
market_open = eastern.localize(
datetime.combine(date_eastern.date(), datetime.strptime("09:30", "%H:%M").time())
)
market_close = eastern.localize(
datetime.combine(date_eastern.date(), datetime.strptime("16:00", "%H:%M").time())
)
return market_open, market_close
def validate_market_time(dt: datetime) -> tuple[datetime, bool]:
"""
Validate if time is during market hours, adjust if needed
Returns: (adjusted_datetime, was_adjusted)
"""
pacific = pytz.timezone('US/Pacific')
eastern = pytz.timezone('US/Eastern')
# Ensure datetime is timezone-aware
if dt.tzinfo is None:
dt = pacific.localize(dt)
dt_eastern = dt.astimezone(eastern)
market_open, market_close = get_market_hours(dt_eastern)
if dt_eastern < market_open:
return market_open.astimezone(pacific), True
elif dt_eastern > market_close:
return market_close.astimezone(pacific), True
return dt, False
def get_datetime_input(prompt: str, default: datetime = None) -> Optional[datetime]:
"""Get date and time input in Pacific time"""
pacific = pytz.timezone('US/Pacific')
while True:
try:
if default:
print(f"Press Enter for current time ({default.strftime('%Y-%m-%d %H:%M')})")
date_str = input(f"{prompt} (YYYY-MM-DD HH:MM, q to quit): ").strip()
if date_str.lower() in ['q', 'quit', 'exit']:
return None
if not date_str and default:
dt = default
else:
dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M")
# Make datetime timezone-aware (Pacific)
dt = pacific.localize(dt)
# Validate market hours
adjusted_dt, was_adjusted = validate_market_time(dt)
if was_adjusted:
print(f"\nWarning: Time adjusted to market hours (Eastern)")
print(f"Original (Pacific): {dt.strftime('%Y-%m-%d %H:%M %Z')}")
print(f"Adjusted (Pacific): {adjusted_dt.strftime('%Y-%m-%d %H:%M %Z')}")
print(f"Adjusted (Eastern): {adjusted_dt.astimezone(pytz.timezone('US/Eastern')).strftime('%Y-%m-%d %H:%M %Z')}")
if input("Accept adjusted time? (y/n): ").lower() != 'y':
continue
return adjusted_dt
except ValueError:
print("Invalid format. Please use YYYY-MM-D HH:MM")
def create_trades_table():
with create_client() as client:
query = """
CREATE TABLE IF NOT EXISTS stock_db.trades (
id UInt32,
position_id String,
ticker String,
entry_date DateTime,
shares UInt32,
entry_price Float64,
target_price Nullable(Float64),
stop_loss Nullable(Float64),
strategy Nullable(String),
order_type String,
direction String,
followed_rules Nullable(UInt8),
entry_reason Nullable(String),
exit_price Nullable(Float64),
exit_date Nullable(DateTime),
exit_reason Nullable(String),
notes Nullable(String),
created_at DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (position_id, id, entry_date)
"""
client.command(query)
def generate_id() -> int:
"""Generate a unique ID for the trade"""
return int(datetime.now().timestamp() * 1000)
def generate_position_id(ticker: str) -> str:
"""Generate a unique position ID for grouping related trades"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
return f"{ticker}_{timestamp}"
def get_position_summary(ticker: str) -> dict:
"""Get summary of existing positions for a ticker"""
with create_client() as client:
query = f"""
SELECT
position_id,
sum(shares) as total_shares,
sum(shares * entry_price) / sum(shares) as avg_entry_price,
min(entry_date) as first_entry,
max(entry_date) as last_entry,
count() as num_orders,
any(target_price) as target_price,
any(stop_loss) as stop_loss,
any(strategy) as strategy
FROM stock_db.trades
WHERE ticker = '{ticker}'
AND exit_price IS NULL
GROUP BY position_id
ORDER BY first_entry DESC
"""
result = client.query(query).result_rows
columns = ['position_id', 'total_shares', 'avg_entry_price',
'first_entry', 'last_entry', 'num_orders',
'target_price', 'stop_loss', 'strategy']
return [dict(zip(columns, row)) for row in result]
def get_order_type() -> Optional[str]:
"""Get order type from user"""
while True:
print("\nOrder Type:")
print("1. Market")
print("2. Limit")
print("q. Quit")
choice = input("Select order type (1-2, q to quit): ")
if choice.lower() in ['q', 'quit', 'exit']:
return None
elif choice == "1":
return "Market"
elif choice == "2":
return "Limit"
print("Invalid choice. Please try again.")
def add_trade(trade: TradeEntry):
with create_client() as client:
query = f"""
INSERT INTO stock_db.trades (
id, position_id, ticker, entry_date, shares, entry_price, target_price, stop_loss,
strategy, order_type, direction, followed_rules, entry_reason, exit_price, exit_date,
exit_reason, notes
) VALUES (
{generate_id()},
'{trade.position_id}',
'{trade.ticker}',
'{trade.entry_date.strftime('%Y-%m-%d %H:%M:%S')}',
{trade.shares},
{trade.entry_price},
{trade.target_price if trade.target_price else 'NULL'},
{trade.stop_loss if trade.stop_loss else 'NULL'},
{f"'{trade.strategy}'" if trade.strategy else 'NULL'},
'{trade.order_type}',
'{trade.direction}',
{1 if trade.followed_rules else 0},
{f"'{trade.entry_reason}'" if trade.entry_reason else 'NULL'},
{trade.exit_price if trade.exit_price else 'NULL'},
{f"'{trade.exit_date.strftime('%Y-%m-%d %H:%M:%S')}'" if trade.exit_date else 'NULL'},
{f"'{trade.exit_reason}'" if trade.exit_reason else 'NULL'},
{f"'{trade.notes}'" if trade.notes else 'NULL'}
)
"""
client.command(query)
def update_trade(trade_id: int, updates: dict):
"""
Update trade details
Args:
trade_id (int): ID of trade to update
updates (dict): Dictionary of fields and values to update
"""
with create_client() as client:
# If trying to update entry_date, we need to delete and reinsert
if 'entry_date' in updates:
# First get the full trade data
query = f"SELECT * FROM stock_db.trades WHERE id = {trade_id}"
result = client.query(query).result_rows
if not result:
raise Exception("Trade not found")
# Delete the existing trade
client.command(f"ALTER TABLE stock_db.trades DELETE WHERE id = {trade_id}")
# Prepare the new trade data
columns = ['id', 'position_id', 'ticker', 'entry_date', 'shares', 'entry_price',
'target_price', 'stop_loss', 'strategy', 'order_type', 'followed_rules',
'entry_reason', 'exit_price', 'exit_date', 'exit_reason', 'notes', 'created_at']
trade_data = dict(zip(columns, result[0]))
trade_data.update(updates)
# Insert the updated trade
query = f"""
INSERT INTO stock_db.trades (
id, position_id, ticker, entry_date, shares, entry_price, target_price,
stop_loss, strategy, order_type, followed_rules, entry_reason, exit_price,
exit_date, exit_reason, notes
) VALUES (
{trade_id},
'{trade_data['position_id']}',
'{trade_data['ticker']}',
'{trade_data['entry_date'].strftime('%Y-%m-%d %H:%M:%S')}',
{trade_data['shares']},
{trade_data['entry_price']},
{trade_data['target_price']},
{trade_data['stop_loss']},
'{trade_data['strategy']}',
'{trade_data['order_type']}',
{1 if trade_data['followed_rules'] else 0},
{f"'{trade_data['entry_reason']}'" if trade_data['entry_reason'] else 'NULL'},
{trade_data['exit_price'] if trade_data['exit_price'] else 'NULL'},
{f"'{trade_data['exit_date'].strftime('%Y-%m-%d %H:%M:%S')}'" if trade_data['exit_date'] else 'NULL'},
{f"'{trade_data['exit_reason']}'" if trade_data['exit_reason'] else 'NULL'},
{f"'{trade_data['notes']}'" if trade_data['notes'] else 'NULL'}
)
"""
client.command(query)
else:
# For non-key columns, we can use regular UPDATE
update_statements = []
for field, value in updates.items():
if isinstance(value, str):
update_statements.append(f"{field} = '{value}'")
elif isinstance(value, datetime):
update_statements.append(f"{field} = '{value.strftime('%Y-%m-%d %H:%M:%S')}'")
elif value is None:
update_statements.append(f"{field} = NULL")
else:
update_statements.append(f"{field} = {value}")
update_clause = ", ".join(update_statements)
query = f"""
ALTER TABLE stock_db.trades
UPDATE {update_clause}
WHERE id = {trade_id}
"""
client.command(query)
def get_open_trades_summary() -> dict:
"""Get summary of all open trades grouped by ticker"""
with create_client() as client:
query = """
SELECT
ticker,
sum(shares) as total_shares,
sum(shares * entry_price) / sum(shares) as avg_entry_price,
min(entry_date) as first_entry,
max(entry_date) as last_entry,
count() as num_orders,
groupArray(position_id) as position_ids
FROM stock_db.trades
WHERE exit_price IS NULL
GROUP BY ticker
ORDER BY ticker ASC
"""
result = client.query(query).result_rows
columns = ['ticker', 'total_shares', 'avg_entry_price',
'first_entry', 'last_entry', 'num_orders', 'position_ids']
return [dict(zip(columns, row)) for row in result]
def get_open_trades():
with create_client() as client:
query = "SELECT * FROM stock_db.trades WHERE exit_price IS NULL ORDER BY entry_date DESC"
result = client.query(query).result_rows
columns = ['id', 'position_id', 'ticker', 'entry_date', 'shares', 'entry_price', 'target_price',
'stop_loss', 'strategy', 'order_type', 'followed_rules', 'entry_reason', 'exit_price',
'exit_date', 'exit_reason', 'notes', 'created_at']
return [dict(zip(columns, row)) for row in result]
def get_current_prices(tickers: list) -> dict:
"""Get current prices for multiple tickers using yfinance"""
prices = {}
for ticker in tickers:
try:
stock = yf.Ticker(ticker)
prices[ticker] = stock.fast_info["last_price"]
except Exception as e:
print(f"Error getting price for {ticker}: {e}")
return prices
def delete_trade(trade_id: int) -> bool:
"""
Delete a trade from the database
Args:
trade_id (int): ID of trade to delete
Returns:
bool: True if deletion was successful
"""
try:
with create_client() as client:
query = f"""
ALTER TABLE stock_db.trades
DELETE WHERE id = {trade_id}
"""
client.command(query)
return True
except Exception as e:
print(f"Error deleting trade: {e}")
return False
def get_trade_history(limit: int = 50):
with create_client() as client:
query = f"""
SELECT * FROM stock_db.trades
WHERE exit_price IS NOT NULL
ORDER BY exit_date DESC
LIMIT {limit}
"""
result = client.query(query).result_rows
columns = ['id', 'position_id', 'ticker', 'entry_date', 'shares', 'entry_price', 'target_price',
'stop_loss', 'strategy', 'order_type', 'followed_rules', 'entry_reason', 'exit_price',
'exit_date', 'exit_reason', 'notes', 'created_at']
return [dict(zip(columns, row)) for row in result]
def journal_menu():
"""Trading journal menu interface"""
create_trades_table() # Ensure table exists
while True:
print("\nTrading Journal")
print("1. Add New Trade")
print("2. Update Existing Trade")
print("3. View Open Trades")
print("4. View Trade History")
print("5. Delete Trade")
print("6. Return to Main Menu")
choice = input("\nSelect an option (1-5): ")
if choice == "1":
ticker = get_user_input("Enter ticker symbol:", str)
if ticker is None:
continue
ticker = ticker.upper()
# Ask if this is a buy or sell order
print("\nOrder Direction:")
print("1. Buy")
print("2. Sell")
direction = get_user_input("Select direction (1-2):", str)
if direction is None:
continue
if direction not in ["1", "2"]:
print("Invalid direction")
continue
if direction == "2": # Sell order
shares = get_user_input("Enter number of shares to sell:", int)
if shares is None:
continue
exit_price = get_user_input("Enter exit price:", float)
if exit_price is None:
continue
exit_date = get_datetime_input("Enter exit date and time", default=datetime.now())
if exit_date is None:
continue
order_type = get_order_type()
if order_type is None:
continue
followed_rules = get_user_input("Did you follow your rules? (y/n):", bool)
if followed_rules is None:
continue
exit_reason = input("Enter exit reason: ")
notes = input("Additional notes (optional): ") or None
if handle_sell_order(
ticker=ticker,
shares_to_sell=shares,
exit_price=exit_price,
exit_date=exit_date,
order_type=order_type,
followed_rules=followed_rules,
exit_reason=exit_reason,
notes=notes
):
print("Sell order processed successfully!")
continue
# Show existing positions for this ticker
existing_positions = get_position_summary(ticker)
if existing_positions:
print(f"\nExisting {ticker} Positions:")
for pos in existing_positions:
print(f"\nPosition ID: {pos['position_id']}")
print(f"Total Shares: {pos['total_shares']}")
print(f"Average Entry: ${pos['avg_entry_price']:.2f}")
print(f"First Entry: {pos['first_entry']}")
print(f"Number of Orders: {pos['num_orders']}")
add_to_existing = get_user_input("Add to existing position? (y/n):", bool)
if add_to_existing is None:
continue
if add_to_existing:
position_id = get_user_input("Enter Position ID:", str)
if position_id is None:
continue
else:
position_id = generate_position_id(ticker)
else:
position_id = generate_position_id(ticker)
# Get entry date/time with market hours validation
entry_date = get_datetime_input("Enter entry date and time", default=datetime.now())
if entry_date is None:
continue
shares = get_user_input("Enter number of shares:", int)
if shares is None:
continue
entry_price = get_user_input("Enter entry price:", float)
if entry_price is None:
continue
order_type = get_order_type()
if order_type is None:
continue
# If adding to existing position, get target/stop from existing
if existing_positions and add_to_existing:
# Use existing target and stop loss
target_price = float(input("Enter new target price (or press Enter to keep existing): ") or
existing_positions[0]['target_price'])
stop_loss = float(input("Enter new stop loss (or press Enter to keep existing): ") or
existing_positions[0]['stop_loss'])
strategy = input("Enter strategy name (or press Enter to keep existing): ") or existing_positions[0]['strategy']
else:
target_price = float(input("Enter target price: "))
stop_loss = float(input("Enter stop loss: "))
strategy = input("Enter strategy name: ")
followed_rules = input("Did you follow your rules? (y/n): ").lower() == 'y'
entry_reason = input("Enter entry reason (optional): ") or None
notes = input("Additional notes (optional): ") or None
trade = TradeEntry(
ticker=ticker,
entry_date=entry_date,
shares=shares,
entry_price=entry_price,
target_price=target_price,
stop_loss=stop_loss,
strategy=strategy,
order_type=order_type,
position_id=position_id,
followed_rules=followed_rules,
entry_reason=entry_reason,
notes=notes
)
add_trade(trade)
# Show updated position summary
updated_positions = get_position_summary(ticker)
if updated_positions:
pos = updated_positions[0] # Get the most recent position
print(f"\nUpdated Position Summary for {ticker}:")
print(f"Total Shares: {pos['total_shares']}")
print(f"Average Entry: ${pos['avg_entry_price']:.2f}")
print(f"Expected Profit: ${(target_price - pos['avg_entry_price']) * pos['total_shares']:.2f}")
print(f"Maximum Loss: ${(stop_loss - pos['avg_entry_price']) * pos['total_shares']:.2f}")
print("Trade added successfully!")
elif choice == "2":
open_trades = get_open_trades()
if not open_trades:
print("No open trades to update.")
continue
print("\nOpen Trades:")
for trade in open_trades:
print(f"{trade['id']}: {trade['ticker']} - Entered at ${trade['entry_price']}")
print("\nOpen Trades:")
for trade in open_trades:
print(f"\nID: {trade['id']}")
print(f"Ticker: {trade['ticker']}")
print(f"Entry Date: {trade['entry_date']}")
print(f"Shares: {trade['shares']}")
print(f"Entry Price: ${trade['entry_price']}")
print(f"Target: ${trade['target_price']}")
print(f"Stop Loss: ${trade['stop_loss']}")
print(f"Strategy: {trade['strategy']}")
print("-" * 40)
trade_id = get_user_input("\nEnter trade ID to update:", int)
if trade_id is None:
continue
# Find the trade to update
trade_to_update = next((t for t in open_trades if t['id'] == trade_id), None)
if not trade_to_update:
print("Trade not found.")
continue
print("\nUpdate Trade Fields")
print("Leave blank to keep existing value")
updates = {}
# Entry date
new_date = get_datetime_input("Enter new entry date and time (blank to keep):", None)
if new_date:
updates['entry_date'] = new_date
# Shares
new_shares = get_user_input("Enter new number of shares:", int, allow_empty=True)
if new_shares is not None:
updates['shares'] = new_shares
# Entry price
new_entry = get_user_input("Enter new entry price:", float, allow_empty=True)
if new_entry is not None:
updates['entry_price'] = new_entry
# Target price
new_target = get_user_input("Enter new target price:", float, allow_empty=True)
if new_target is not None:
updates['target_price'] = new_target
# Stop loss
new_stop = get_user_input("Enter new stop loss:", float, allow_empty=True)
if new_stop is not None:
updates['stop_loss'] = new_stop
# Strategy
new_strategy = input("Enter new strategy (blank to keep): ").strip()
if new_strategy:
updates['strategy'] = new_strategy
# Order type
if input("Update order type? (y/n): ").lower() == 'y':
new_order_type = get_order_type()
if new_order_type:
updates['order_type'] = new_order_type
# Notes
new_notes = input("Enter new notes (blank to keep): ").strip()
if new_notes:
updates['notes'] = new_notes
if updates:
try:
update_trade(trade_id, updates)
print("Trade updated successfully!")
except Exception as e:
print(f"Error updating trade: {e}")
else:
print("No updates provided.")
elif choice == "3":
open_trades = get_open_trades()
open_summary = get_open_trades_summary()
if not open_trades:
print("No open trades found.")
else:
# Get current prices for all open positions
unique_tickers = list(set(summary['ticker'] for summary in open_summary))
current_prices = get_current_prices(unique_tickers)
print("\n=== Open Trades Summary ===")
total_portfolio_value = 0
total_paper_pl = 0
for summary in open_summary:
ticker = summary['ticker']
avg_entry = summary['avg_entry_price']
current_price = current_prices.get(ticker)
stop_loss = avg_entry * 0.93 # 7% stop loss
total_shares = summary['total_shares']
position_value = avg_entry * total_shares
max_loss = (avg_entry - stop_loss) * total_shares
print(f"\n{ticker} Summary:")
print(f"Total Shares: {total_shares}")
print(f"Average Entry: ${avg_entry:.2f}")
print(f"Total Position Value: ${position_value:.2f}")
print(f"Combined Stop Loss (7%): ${stop_loss:.2f}")
print(f"Maximum Loss at Stop: ${max_loss:.2f}")
if current_price:
current_value = current_price * total_shares
paper_pl = (current_price - avg_entry) * total_shares
pl_percentage = (paper_pl / position_value) * 100
total_portfolio_value += current_value
total_paper_pl += paper_pl
print(f"Current Price: ${current_price:.2f}")
print(f"Current Value: ${current_value:.2f}")
print(f"Paper P/L: ${paper_pl:.2f} ({pl_percentage:.2f}%)")
print(f"Number of Orders: {summary['num_orders']}")
print(f"Position Duration: {summary['last_entry'] - summary['first_entry']}")
print("-" * 50)
if total_portfolio_value > 0:
print(f"\nTotal Portfolio Value: ${total_portfolio_value:.2f}")
print(f"Total Paper P/L: ${total_paper_pl:.2f}")
print(f"Overall P/L %: {(total_paper_pl / (total_portfolio_value - total_paper_pl)) * 100:.2f}%")
print("\n=== Individual Trades ===")
for trade in open_trades:
ticker = trade['ticker']
current_price = current_prices.get(ticker)
print(f"\nTicker: {ticker}")
print(f"Position ID: {trade['position_id']}")
print(f"Entry Date: {trade['entry_date']}")
print(f"Shares: {trade['shares']}")
print(f"Entry Price: ${trade['entry_price']}")
print(f"Target: ${trade['target_price']}")
print(f"Stop Loss: ${trade['stop_loss']}")
print(f"Strategy: {trade['strategy']}")
print(f"Order Type: {trade['order_type']}")
if current_price:
paper_pl = (current_price - trade['entry_price']) * trade['shares']
pl_percentage = (paper_pl / (trade['entry_price'] * trade['shares'])) * 100
print(f"Current Price: ${current_price:.2f}")
print(f"Paper P/L: ${paper_pl:.2f} ({pl_percentage:.2f}%)")
if trade['entry_reason']:
print(f"Entry Reason: {trade['entry_reason']}")
if trade['notes']:
print(f"Notes: {trade['notes']}")
print("-" * 40)
elif choice == "4":
history = get_trade_history()
if not history:
print("No trade history found.")
else:
print("\nTrade History:")
for trade in history:
profit_loss = (trade['exit_price'] - trade['entry_price']) * trade['shares'] if trade['exit_price'] else None
print(f"\nTicker: {trade['ticker']}")
print(f"Entry: ${trade['entry_price']} on {trade['entry_date']}")
if trade['exit_price']:
print(f"Exit: ${trade['exit_price']} on {trade['exit_date']}")
print(f"P/L: ${profit_loss:.2f}")
print(f"Strategy: {trade['strategy']}")
if trade['notes']:
print(f"Notes: {trade['notes']}")
print("-" * 40)
elif choice == "5":
# Show all trades (both open and closed)
print("\nAll Trades:")
with create_client() as client:
query = "SELECT * FROM stock_db.trades ORDER BY entry_date DESC"
result = client.query(query).result_rows
columns = ['id', 'position_id', 'ticker', 'entry_date', 'shares', 'entry_price',
'target_price', 'stop_loss', 'strategy', 'order_type', 'followed_rules',
'entry_reason', 'exit_price', 'exit_date', 'exit_reason', 'notes', 'created_at']
trades = [dict(zip(columns, row)) for row in result]
for trade in trades:
print(f"\nID: {trade['id']}")
print(f"Ticker: {trade['ticker']}")
print(f"Entry Date: {trade['entry_date']}")
print(f"Shares: {trade['shares']}")
print(f"Entry Price: ${trade['entry_price']}")
if trade['exit_price']:
print(f"Exit Price: ${trade['exit_price']}")
print(f"Exit Date: {trade['exit_date']}")
pl = (trade['exit_price'] - trade['entry_price']) * trade['shares']
print(f"P/L: ${pl:.2f}")
print("-" * 40)
trade_id = get_user_input("\nEnter trade ID to delete:", int)
if trade_id is None:
continue
confirm = input(f"Are you sure you want to delete trade {trade_id}? (y/n): ").lower()
if confirm == 'y':
if delete_trade(trade_id):
print("Trade deleted successfully!")
else:
print("Failed to delete trade.")
else:
print("Delete cancelled.")
elif choice == "6":
break