| |
| """ |
| ็พ่กๆไป็ๆง โ ็ไธญๆฏๅ้ๅทๆฐ๏ผ็ๅค่ชๅจ็ญๅพ
|
| ็จๆณ: python3 monitor.py [ๅทๆฐ้ด้็งๆฐ๏ผ้ป่ฎค60] |
| """ |
|
|
| import json |
| import os |
| import sys |
| import time |
| from datetime import datetime, timedelta |
|
|
| import yfinance as yf |
|
|
| PORTFOLIO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "my_portfolio.json") |
| INITIAL_CASH = 100_000.0 |
| REFRESH_SEC = int(sys.argv[1]) if len(sys.argv) > 1 else 60 |
|
|
|
|
| def load_portfolio(): |
| with open(PORTFOLIO_FILE) as f: |
| return json.load(f) |
|
|
|
|
| def get_prices(symbols): |
| """ๆน้่ทๅไปทๆ ผ๏ผๆฏ้ไธชๅฟซๅพๅค๏ผ""" |
| prices = {} |
| if not symbols: |
| return prices |
| tickers = yf.Tickers(" ".join(symbols)) |
| for sym in symbols: |
| try: |
| info = tickers.tickers[sym].fast_info |
| p = info.get("lastPrice") or info.get("last_price") |
| if p is None: |
| hist = tickers.tickers[sym].history(period="1d") |
| p = hist["Close"].iloc[-1] if not hist.empty else 0 |
| prices[sym] = round(float(p), 2) |
| except Exception: |
| prices[sym] = 0 |
| return prices |
|
|
|
|
| def is_market_open(): |
| """ๅคๆญ็พ่กๆฏๅฆๅผ็๏ผ็พไธๆถ้ด ๅจไธ-ๅจไบ 9:30-16:00๏ผ""" |
| try: |
| from zoneinfo import ZoneInfo |
| except ImportError: |
| from backports.zoneinfo import ZoneInfo |
|
|
| et = datetime.now(ZoneInfo("America/New_York")) |
| |
| if et.weekday() >= 5: |
| return False, et |
| |
| market_open = et.replace(hour=9, minute=30, second=0, microsecond=0) |
| market_close = et.replace(hour=16, minute=0, second=0, microsecond=0) |
| return market_open <= et <= market_close, et |
|
|
|
|
| def time_to_next_open(): |
| """่ฎก็ฎ่ท็ฆปไธๆฌกๅผ็็ๆถ้ด""" |
| try: |
| from zoneinfo import ZoneInfo |
| except ImportError: |
| from backports.zoneinfo import ZoneInfo |
|
|
| et = datetime.now(ZoneInfo("America/New_York")) |
| |
| target = et.replace(hour=9, minute=30, second=0, microsecond=0) |
|
|
| if et.weekday() < 5 and et < target: |
| |
| pass |
| else: |
| |
| days_ahead = 1 |
| while True: |
| target += timedelta(days=1) |
| if target.weekday() < 5: |
| break |
| days_ahead += 1 |
| target = target.replace(hour=9, minute=30, second=0, microsecond=0) |
|
|
| diff = target - et |
| return diff |
|
|
|
|
| def clear_screen(): |
| os.system("clear" if os.name != "nt" else "cls") |
|
|
|
|
| def display(portfolio, prices, et_now, is_open): |
| clear_screen() |
| status = "๐ข ๅผ็ไธญ" if is_open else "๐ด ๅทฒไผๅธ" |
| print(f""" |
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| โ ๐ ็พ่กๆจกๆๆ่ต โ ๅฎๆถ็ๆง {status} โ |
| โ ็พไธๆถ้ด: {et_now.strftime('%Y-%m-%d %H:%M:%S'):<20} ๅทๆฐ้ด้: {REFRESH_SEC}็ง โ |
| โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ""") |
|
|
| total_market = 0 |
| total_cost = 0 |
|
|
| if portfolio["holdings"]: |
| print(f"โ {'่ก็ฅจ':<7} {'่กๆฐ':>5} {'ๆๆฌ':>9} {'็ฐไปท':>9} {'ๅธๅผ':>11} {'็ไบ':>11} {'ๆถจ่ท':>7} โ") |
| print(f"โ {'โ'*62} โ") |
|
|
| for sym in sorted(portfolio["holdings"]): |
| info = portfolio["holdings"][sym] |
| price = prices.get(sym, info["avg_cost"]) |
| mkt = info["shares"] * price |
| cost = info["shares"] * info["avg_cost"] |
| pnl = mkt - cost |
| pct = (pnl / cost * 100) if cost > 0 else 0 |
| sign = "+" if pnl >= 0 else "" |
| color_pnl = f"{sign}{pnl:,.0f}" |
| color_pct = f"{sign}{pct:.1f}%" |
|
|
| total_market += mkt |
| total_cost += cost |
|
|
| print(f"โ {sym:<7} {info['shares']:>5} {info['avg_cost']:>9.2f} {price:>9.2f} {mkt:>11,.2f} {color_pnl:>11} {color_pct:>7} โ") |
| else: |
| print("โ ๏ผ็ฉบไป๏ผ โ") |
|
|
| total_assets = portfolio["cash"] + total_market |
| total_pnl = total_assets - INITIAL_CASH |
| total_pct = (total_pnl / INITIAL_CASH * 100) |
| sign = "+" if total_pnl >= 0 else "" |
|
|
| print(f"โ {'โ'*62} โ") |
| print(f"โ ๐ฐ ็ฐ้: ${portfolio['cash']:>11,.2f} โ") |
| print(f"โ ๐ ๆไป: ${total_market:>11,.2f} โ") |
| print(f"โ ๐ผ ๆป่ตไบง: ${total_assets:>11,.2f} ๆป็ไบ: {sign}${total_pnl:>10,.2f} ({sign}{total_pct:.1f}%) โ") |
| print(f"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ") |
|
|
| if not is_open: |
| delta = time_to_next_open() |
| hours = int(delta.total_seconds() // 3600) |
| mins = int((delta.total_seconds() % 3600) // 60) |
| print(f"\n โณ ่ท็ฆปไธๆฌกๅผ็: {hours}ๅฐๆถ{mins}ๅ้๏ผไผๅธๆ้ดๆฏ5ๅ้ๅทๆฐไธๆฌก๏ผ") |
|
|
| print(f"\n ๆ Ctrl+C ้ๅบ") |
|
|
|
|
| def main(): |
| print(" ๐ ๅฏๅจ็ๆงไธญ...") |
|
|
| while True: |
| try: |
| portfolio = load_portfolio() |
| symbols = list(portfolio["holdings"].keys()) |
| prices = get_prices(symbols) |
| is_open, et_now = is_market_open() |
|
|
| display(portfolio, prices, et_now, is_open) |
|
|
| |
| wait = REFRESH_SEC if is_open else 300 |
| time.sleep(wait) |
|
|
| except KeyboardInterrupt: |
| print("\n\n ๐ ็ๆงๅทฒๅๆญข\n") |
| break |
| except Exception as e: |
| print(f"\n โ ๏ธ ๅบ้: {e}๏ผ{REFRESH_SEC}็งๅ้่ฏ...") |
| time.sleep(REFRESH_SEC) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|