206 lines
6.5 KiB
Python
Executable File
206 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Position Updater Script
|
|
|
|
This script:
|
|
1. Fetches current prices for all open positions
|
|
2. Updates positions with latest price data
|
|
3. Calculates return % for each position
|
|
4. Can be run manually or via cron for continuous monitoring
|
|
|
|
Usage:
|
|
python scripts/update_positions.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
|
|
from datetime import datetime
|
|
|
|
import yfinance as yf
|
|
|
|
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
|
|
from tradingagents.utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def fetch_current_prices(tickers):
|
|
"""
|
|
Fetch current prices for given tickers using yfinance.
|
|
|
|
Handles both single and multiple tickers with appropriate error handling.
|
|
|
|
Args:
|
|
tickers: List of ticker symbols
|
|
|
|
Returns:
|
|
Dictionary mapping ticker to current price (or None if fetch failed)
|
|
"""
|
|
prices = {}
|
|
|
|
if not tickers:
|
|
return prices
|
|
|
|
# Try to download all tickers at once for efficiency
|
|
try:
|
|
if len(tickers) == 1:
|
|
# Single ticker - yfinance returns Series instead of DataFrame
|
|
ticker = tickers[0]
|
|
data = yf.download(
|
|
ticker,
|
|
period="1d",
|
|
progress=False,
|
|
auto_adjust=True,
|
|
)
|
|
|
|
if not data.empty:
|
|
# For single ticker with period='1d', get the latest close
|
|
prices[ticker] = float(data["Close"].iloc[-1])
|
|
else:
|
|
logger.warning(f"Could not fetch data for {ticker}")
|
|
prices[ticker] = None
|
|
|
|
else:
|
|
# Multiple tickers - yfinance returns DataFrame with MultiIndex
|
|
data = yf.download(
|
|
tickers,
|
|
period="1d",
|
|
progress=False,
|
|
auto_adjust=True,
|
|
)
|
|
|
|
if not data.empty:
|
|
# Get the latest close for each ticker
|
|
if len(tickers) > 1:
|
|
for ticker in tickers:
|
|
if ticker in data.columns:
|
|
close_price = data[ticker]["Close"]
|
|
if not close_price.empty:
|
|
prices[ticker] = float(close_price.iloc[-1])
|
|
else:
|
|
prices[ticker] = None
|
|
else:
|
|
prices[ticker] = None
|
|
else:
|
|
# Edge case: single ticker in batch download
|
|
if "Close" in data.columns:
|
|
prices[tickers[0]] = float(data["Close"].iloc[-1])
|
|
else:
|
|
prices[tickers[0]] = None
|
|
else:
|
|
for ticker in tickers:
|
|
prices[ticker] = None
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Batch download failed: {e}")
|
|
# Fall back to per-ticker download
|
|
for ticker in tickers:
|
|
try:
|
|
data = yf.download(
|
|
ticker,
|
|
period="1d",
|
|
progress=False,
|
|
auto_adjust=True,
|
|
)
|
|
if not data.empty:
|
|
prices[ticker] = float(data["Close"].iloc[-1])
|
|
else:
|
|
prices[ticker] = None
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch price for {ticker}: {e}")
|
|
prices[ticker] = None
|
|
|
|
return prices
|
|
|
|
|
|
def main():
|
|
"""
|
|
Main function to update all open positions with current prices.
|
|
|
|
Process:
|
|
1. Initialize PositionTracker
|
|
2. Load all open positions
|
|
3. Get unique tickers
|
|
4. Fetch current prices via yfinance
|
|
5. Update each position with new price
|
|
6. Save updated positions
|
|
7. Print progress messages
|
|
"""
|
|
logger.info(
|
|
"""
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ TradingAgents - Position Updater ║
|
|
╚══════════════════════════════════════════════════════════════╝""".strip()
|
|
)
|
|
|
|
# Initialize position tracker
|
|
tracker = PositionTracker(data_dir="data")
|
|
|
|
# Load all open positions
|
|
logger.info("📂 Loading open positions...")
|
|
positions = tracker.load_all_open_positions()
|
|
|
|
if not positions:
|
|
logger.info("✅ No open positions to update.")
|
|
return
|
|
|
|
logger.info(f"✅ Found {len(positions)} open position(s)")
|
|
|
|
# Get unique tickers
|
|
tickers = list({pos["ticker"] for pos in positions})
|
|
logger.info(f"📊 Fetching current prices for {len(tickers)} unique ticker(s)...")
|
|
logger.info(f"Tickers: {', '.join(sorted(tickers))}")
|
|
|
|
# Fetch current prices
|
|
prices = fetch_current_prices(tickers)
|
|
|
|
# Update positions and track results
|
|
updated_count = 0
|
|
failed_count = 0
|
|
|
|
for position in positions:
|
|
ticker = position["ticker"]
|
|
current_price = prices.get(ticker)
|
|
|
|
if current_price is None:
|
|
logger.error(f"{ticker}: Failed to fetch price - position not updated")
|
|
failed_count += 1
|
|
continue
|
|
|
|
# Update position with new price
|
|
entry_price = position["entry_price"]
|
|
return_pct = ((current_price - entry_price) / entry_price) * 100
|
|
|
|
# Update the position
|
|
position = tracker.update_position_price(position, current_price)
|
|
|
|
# Save the updated position
|
|
tracker.save_position(position)
|
|
|
|
# Log progress
|
|
return_symbol = "📈" if return_pct >= 0 else "📉"
|
|
logger.info(
|
|
f"{return_symbol} {ticker:6} | Price: ${current_price:8.2f} | Return: {return_pct:+7.2f}%"
|
|
)
|
|
updated_count += 1
|
|
|
|
# Summary
|
|
logger.info("=" * 60)
|
|
logger.info("✅ Update Summary:")
|
|
logger.info(f"Updated: {updated_count}/{len(positions)} positions")
|
|
logger.info(f"Failed: {failed_count}/{len(positions)} positions")
|
|
logger.info(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
logger.info("=" * 60)
|
|
|
|
if updated_count > 0:
|
|
logger.info("🎉 Position update complete!")
|
|
else:
|
|
logger.warning("No positions were successfully updated.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|