From 8ea233ff7cc92134b8325e7177441963d3664fcf Mon Sep 17 00:00:00 2001 From: "Bobby (aider)" Date: Mon, 10 Feb 2025 10:31:16 -0800 Subject: [PATCH] feat: Add FIFO sell order handling in trade journal --- src/trading/journal.py | 141 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/trading/journal.py b/src/trading/journal.py index f2a8cfa..f85c6e0 100644 --- a/src/trading/journal.py +++ b/src/trading/journal.py @@ -7,6 +7,98 @@ 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_exit( + trade_id=position['id'], + exit_price=exit_price, + exit_date=exit_date, + followed_rules=followed_rules, + 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 = """ @@ -343,6 +435,55 @@ def journal_menu(): 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 + + shares = get_user_input("Enter number of shares:", int) + if shares is None: + continue + + if direction == "2": # Sell order + 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: