834 lines
34 KiB
Python
834 lines
34 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: float
|
|
stop_loss: float
|
|
strategy: str
|
|
order_type: str # New field for Market/Limit
|
|
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 # New field to group related orders
|
|
|
|
@property
|
|
def expected_profit_loss(self) -> float:
|
|
return (self.target_price - self.entry_price) * self.shares
|
|
|
|
@property
|
|
def max_loss(self) -> float:
|
|
return (self.stop_loss - self.entry_price) * self.shares
|
|
|
|
@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 Float64,
|
|
stop_loss Float64,
|
|
strategy String,
|
|
order_type 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, 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},
|
|
{trade.stop_loss},
|
|
'{trade.strategy}',
|
|
'{trade.order_type}',
|
|
{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 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. 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
|
|
|
|
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:
|
|
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=datetime.now(),
|
|
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":
|
|
break
|