Files
workspace/projects/market-watch/trader.py
Case be43231c3f Market Watch: multiplayer GARP paper trading simulator
- Game engine with multiplayer support (create games, join, leaderboard)
- GARP stock screener (S&P 500 + 400 MidCap, 900+ tickers)
- Automated trading logic for AI player (Case)
- Web portal at marketwatch.local:8889 with dark theme
- Systemd timer for Mon-Fri market hours
- Telegram alerts on trades and daily summary
- Stock analysis deep dive data (BAC, CFG, FITB, INCY)
- Expanded scan results (22 GARP candidates)
- Craigslist account setup + credentials
2026-02-08 15:18:41 -06:00

192 lines
6.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""GARP trading decision engine — multiplayer aware."""
import json
import os
from datetime import datetime
import yfinance as yf
import game_engine
import scanner
MAX_POSITIONS = 15
MAX_POSITION_PCT = 0.10
RSI_BUY_LIMIT = 70
RSI_SELL_LIMIT = 80
NEAR_HIGH_PCT = 2.0
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "logs")
def log_decision(action, ticker, reason, details=None):
os.makedirs(LOG_DIR, exist_ok=True)
entry = {
"timestamp": datetime.now().isoformat(),
"action": action,
"ticker": ticker,
"reason": reason,
"details": details or {},
}
log_file = os.path.join(LOG_DIR, f"{datetime.now().strftime('%Y-%m-%d')}.json")
logs = []
if os.path.exists(log_file):
with open(log_file) as f:
logs = json.load(f)
logs.append(entry)
with open(log_file, "w") as f:
json.dump(logs, f, indent=2, default=str)
return entry
def update_all_prices(game_id, username):
"""Update current prices for all held positions."""
p = game_engine.get_portfolio(game_id, username)
updated = []
for ticker in p["positions"]:
try:
stock = yf.Ticker(ticker)
hist = stock.history(period="5d")
if not hist.empty:
price = float(hist["Close"].iloc[-1])
game_engine.update_price(game_id, username, ticker, price)
updated.append((ticker, price))
except Exception as e:
print(f" Warning: Could not update {ticker}: {e}")
return updated
def check_sell_signals(game_id, username):
"""Check existing positions for sell signals."""
p = game_engine.get_portfolio(game_id, username)
sells = []
if not p["positions"]:
return sells
latest_scan = scanner.load_latest_scan()
scan_tickers = set()
if latest_scan:
scan_tickers = {c["ticker"] for c in latest_scan.get("candidates", [])}
for ticker, pos in list(p["positions"].items()):
sell_reason = None
if pos["current_price"] <= pos.get("trailing_stop", 0):
sell_reason = f"Trailing stop hit (stop={pos.get('trailing_stop', 0):.2f}, price={pos['current_price']:.2f})"
if not sell_reason:
try:
stock = yf.Ticker(ticker)
hist = stock.history(period="3mo")
if not hist.empty and len(hist) >= 15:
rsi = scanner.compute_rsi(hist["Close"].values)
if rsi and rsi > RSI_SELL_LIMIT:
sell_reason = f"RSI overbought ({rsi:.1f} > {RSI_SELL_LIMIT})"
except:
pass
if not sell_reason and latest_scan and ticker not in scan_tickers:
sell_reason = f"No longer passes GARP filter"
if sell_reason:
result = game_engine.sell(game_id, username, ticker, price=pos["current_price"], reason=sell_reason)
log_entry = log_decision("SELL", ticker, sell_reason, result)
sells.append(log_entry)
print(f" SELL {ticker}: {sell_reason}")
return sells
def check_buy_signals(game_id, username, candidates=None):
"""Check scan candidates for buy signals."""
p = game_engine.get_portfolio(game_id, username)
buys = []
if p["num_positions"] >= MAX_POSITIONS:
print(f" Max positions reached ({MAX_POSITIONS}), skipping buys")
return buys
if candidates is None:
latest_scan = scanner.load_latest_scan()
if not latest_scan:
print(" No scan data available")
return buys
candidates = latest_scan.get("candidates", [])
position_size = p["total_value"] / MAX_POSITIONS
max_per_position = p["total_value"] * MAX_POSITION_PCT
existing_tickers = set(p["positions"].keys())
for c in candidates:
if p["num_positions"] + len(buys) >= MAX_POSITIONS:
break
ticker = c["ticker"]
if ticker in existing_tickers:
continue
rsi = c.get("rsi")
if rsi and rsi > RSI_BUY_LIMIT:
log_decision("SKIP", ticker, f"RSI too high ({rsi:.1f} > {RSI_BUY_LIMIT})")
continue
pct_from_high = c.get("pct_from_52wk_high", 0)
if pct_from_high < NEAR_HIGH_PCT:
log_decision("SKIP", ticker, f"Too close to 52wk high ({pct_from_high:.1f}% away)")
continue
price = c["price"]
# Refresh cash from current portfolio state
current_p = game_engine.get_portfolio(game_id, username)
amount = min(position_size, max_per_position, current_p["cash"])
if amount < price:
continue
shares = int(amount / price)
if shares < 1:
continue
reason = (f"GARP signal: PE={c['trailing_pe']}, FwdPE={c['forward_pe']}, "
f"RevGr={c['revenue_growth']}%, EPSGr={c['earnings_growth']}%, RSI={rsi}")
result = game_engine.buy(game_id, username, ticker, shares, price, reason=reason)
if result["success"]:
log_entry = log_decision("BUY", ticker, reason, result)
buys.append(log_entry)
print(f" BUY {ticker}: {shares} shares @ ${price:.2f} = ${shares * price:,.2f}")
else:
log_decision("SKIP", ticker, f"Buy failed: {result.get('error', 'unknown')}")
return buys
def run_trading_logic(game_id, username, candidates=None):
"""Run full trading cycle for a player."""
print(f"\n--- Trading Logic [{username}@{game_id}] ---")
print("\nUpdating prices...")
updated = update_all_prices(game_id, username)
for ticker, price in updated:
print(f" {ticker}: ${price:.2f}")
print("\nChecking sell signals...")
sells = check_sell_signals(game_id, username)
if not sells:
print(" No sell signals")
print("\nChecking buy signals...")
buys = check_buy_signals(game_id, username, candidates)
if not buys:
print(" No buy signals")
return {"sells": sells, "buys": buys, "price_updates": len(updated)}
if __name__ == "__main__":
gid = game_engine.get_default_game_id()
if gid:
run_trading_logic(gid, "case")
else:
print("No default game found. Run game_engine.py first.")