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
This commit is contained in:
191
projects/market-watch/trader.py
Executable file
191
projects/market-watch/trader.py
Executable file
@ -0,0 +1,191 @@
|
||||
#!/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.")
|
||||
Reference in New Issue
Block a user