Crypto Market Watch game - VWAP+RSI scanner, paper trading, systemd timer
This commit is contained in:
593
projects/crypto-watch/game_engine.py
Normal file
593
projects/crypto-watch/game_engine.py
Normal file
@ -0,0 +1,593 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Crypto Market Watch - Paper Trading Game Engine
|
||||
Scans top 150 cryptos using VWAP + RSI + volume analysis.
|
||||
Makes autonomous paper trades with full tracking.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
|
||||
DATA_DIR = Path(__file__).parent / "data"
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
PORTFOLIO_FILE = DATA_DIR / "portfolio.json"
|
||||
TRADES_FILE = DATA_DIR / "trades.json"
|
||||
SNAPSHOTS_FILE = DATA_DIR / "snapshots.json"
|
||||
WATCHLIST_FILE = DATA_DIR / "watchlist.json"
|
||||
|
||||
|
||||
def _load(path, default=None):
|
||||
if path.exists():
|
||||
return json.loads(path.read_text())
|
||||
return default if default is not None else {}
|
||||
|
||||
|
||||
def _save(path, data):
|
||||
path.write_text(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
# ── Portfolio Management ──
|
||||
|
||||
def get_portfolio():
|
||||
default = {
|
||||
"cash": 100000.0,
|
||||
"positions": {},
|
||||
"starting_balance": 100000.0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
return _load(PORTFOLIO_FILE, default)
|
||||
|
||||
|
||||
def save_portfolio(portfolio):
|
||||
_save(PORTFOLIO_FILE, portfolio)
|
||||
|
||||
|
||||
def get_trades():
|
||||
return _load(TRADES_FILE, [])
|
||||
|
||||
|
||||
def save_trades(trades):
|
||||
_save(TRADES_FILE, trades)
|
||||
|
||||
|
||||
def buy(symbol, price, amount_usd, reason=""):
|
||||
"""Buy a crypto position."""
|
||||
portfolio = get_portfolio()
|
||||
trades = get_trades()
|
||||
|
||||
if amount_usd > portfolio["cash"]:
|
||||
return None, "Insufficient cash"
|
||||
|
||||
qty = amount_usd / price
|
||||
portfolio["cash"] -= amount_usd
|
||||
|
||||
pos = portfolio["positions"].get(symbol, {
|
||||
"qty": 0, "avg_price": 0, "total_cost": 0
|
||||
})
|
||||
|
||||
new_total_cost = pos["total_cost"] + amount_usd
|
||||
new_qty = pos["qty"] + qty
|
||||
pos["avg_price"] = new_total_cost / new_qty if new_qty > 0 else 0
|
||||
pos["qty"] = new_qty
|
||||
pos["total_cost"] = new_total_cost
|
||||
pos["last_buy"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
portfolio["positions"][symbol] = pos
|
||||
save_portfolio(portfolio)
|
||||
|
||||
trade = {
|
||||
"id": len(trades) + 1,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"action": "BUY",
|
||||
"symbol": symbol,
|
||||
"price": price,
|
||||
"qty": qty,
|
||||
"amount_usd": amount_usd,
|
||||
"reason": reason,
|
||||
}
|
||||
trades.append(trade)
|
||||
save_trades(trades)
|
||||
return trade, None
|
||||
|
||||
|
||||
def sell(symbol, price, pct=100, reason=""):
|
||||
"""Sell a position (partial or full)."""
|
||||
portfolio = get_portfolio()
|
||||
trades = get_trades()
|
||||
|
||||
pos = portfolio["positions"].get(symbol)
|
||||
if not pos or pos["qty"] <= 0:
|
||||
return None, f"No position in {symbol}"
|
||||
|
||||
sell_qty = pos["qty"] * (pct / 100)
|
||||
sell_value = sell_qty * price
|
||||
cost_basis = pos["avg_price"] * sell_qty
|
||||
pnl = sell_value - cost_basis
|
||||
pnl_pct = (pnl / cost_basis) * 100 if cost_basis > 0 else 0
|
||||
|
||||
portfolio["cash"] += sell_value
|
||||
pos["qty"] -= sell_qty
|
||||
pos["total_cost"] -= cost_basis
|
||||
|
||||
if pos["qty"] < 0.0000001:
|
||||
del portfolio["positions"][symbol]
|
||||
else:
|
||||
portfolio["positions"][symbol] = pos
|
||||
|
||||
save_portfolio(portfolio)
|
||||
|
||||
trade = {
|
||||
"id": len(trades) + 1,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"action": "SELL",
|
||||
"symbol": symbol,
|
||||
"price": price,
|
||||
"qty": sell_qty,
|
||||
"amount_usd": sell_value,
|
||||
"pnl": round(pnl, 2),
|
||||
"pnl_pct": round(pnl_pct, 2),
|
||||
"reason": reason,
|
||||
}
|
||||
trades.append(trade)
|
||||
save_trades(trades)
|
||||
return trade, None
|
||||
|
||||
|
||||
def get_portfolio_value(prices):
|
||||
"""Calculate total portfolio value."""
|
||||
portfolio = get_portfolio()
|
||||
positions_value = 0
|
||||
position_details = []
|
||||
|
||||
for symbol, pos in portfolio["positions"].items():
|
||||
current_price = prices.get(symbol, pos["avg_price"])
|
||||
value = pos["qty"] * current_price
|
||||
pnl = value - pos["total_cost"]
|
||||
pnl_pct = (pnl / pos["total_cost"]) * 100 if pos["total_cost"] > 0 else 0
|
||||
positions_value += value
|
||||
position_details.append({
|
||||
"symbol": symbol,
|
||||
"qty": pos["qty"],
|
||||
"avg_price": pos["avg_price"],
|
||||
"current_price": current_price,
|
||||
"value": round(value, 2),
|
||||
"pnl": round(pnl, 2),
|
||||
"pnl_pct": round(pnl_pct, 2),
|
||||
})
|
||||
|
||||
total_value = portfolio["cash"] + positions_value
|
||||
total_pnl = total_value - portfolio["starting_balance"]
|
||||
total_pnl_pct = (total_pnl / portfolio["starting_balance"]) * 100
|
||||
|
||||
return {
|
||||
"cash": round(portfolio["cash"], 2),
|
||||
"positions_value": round(positions_value, 2),
|
||||
"total_value": round(total_value, 2),
|
||||
"total_pnl": round(total_pnl, 2),
|
||||
"total_pnl_pct": round(total_pnl_pct, 2),
|
||||
"positions": sorted(position_details, key=lambda x: -x["value"]),
|
||||
"num_positions": len(position_details),
|
||||
}
|
||||
|
||||
|
||||
def take_snapshot(prices):
|
||||
"""Save a point-in-time portfolio snapshot."""
|
||||
snapshots = _load(SNAPSHOTS_FILE, [])
|
||||
value = get_portfolio_value(prices)
|
||||
value["timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||
snapshots.append(value)
|
||||
# Keep last 1000
|
||||
_save(SNAPSHOTS_FILE, snapshots[-1000:])
|
||||
return value
|
||||
|
||||
|
||||
# ── Market Data ──
|
||||
|
||||
def get_top_coins(limit=150):
|
||||
"""Fetch top coins by market cap from CoinGecko."""
|
||||
coins = []
|
||||
for page in range(1, (limit // 100) + 2):
|
||||
per_page = min(100, limit - len(coins))
|
||||
if per_page <= 0:
|
||||
break
|
||||
url = (
|
||||
f"https://api.coingecko.com/api/v3/coins/markets?"
|
||||
f"vs_currency=usd&order=market_cap_desc&per_page={per_page}&page={page}"
|
||||
f"&sparkline=false&price_change_percentage=1h,24h,7d"
|
||||
)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
batch = json.loads(resp.read())
|
||||
coins.extend(batch)
|
||||
except Exception as e:
|
||||
print(f"Error fetching page {page}: {e}")
|
||||
break
|
||||
time.sleep(1) # Rate limit
|
||||
return coins
|
||||
|
||||
|
||||
def get_ohlcv(coin_id, days=2):
|
||||
"""Get OHLCV data from CoinGecko for VWAP calculation."""
|
||||
url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/ohlc?vs_currency=usd&days={days}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
raw = json.loads(resp.read())
|
||||
# CoinGecko OHLC: [timestamp, open, high, low, close]
|
||||
return [{"t": r[0], "o": r[1], "h": r[2], "l": r[3], "c": r[4]} for r in raw]
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def get_binance_klines(symbol, interval="1h", limit=48):
|
||||
"""Get klines from Binance US for VWAP + volume."""
|
||||
url = f"https://api.binance.us/api/v3/klines?symbol={symbol}USDT&interval={interval}&limit={limit}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
raw = json.loads(resp.read())
|
||||
return [{"t": k[0], "o": float(k[1]), "h": float(k[2]), "l": float(k[3]),
|
||||
"c": float(k[4]), "v": float(k[5])} for k in raw]
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
# ── Technical Analysis ──
|
||||
|
||||
def calc_vwap(candles):
|
||||
"""Calculate VWAP with standard deviation bands."""
|
||||
if not candles:
|
||||
return None
|
||||
|
||||
cum_tpv = cum_vol = cum_sq = 0
|
||||
for c in candles:
|
||||
vol = c.get("v", 1) # Default volume 1 if not available
|
||||
tp = (c["h"] + c["l"] + c["c"]) / 3
|
||||
cum_tpv += tp * vol
|
||||
cum_vol += vol
|
||||
|
||||
vwap = cum_tpv / cum_vol if cum_vol > 0 else candles[-1]["c"]
|
||||
|
||||
# Standard deviation
|
||||
for c in candles:
|
||||
vol = c.get("v", 1)
|
||||
tp = (c["h"] + c["l"] + c["c"]) / 3
|
||||
cum_sq += vol * (tp - vwap) ** 2
|
||||
|
||||
variance = cum_sq / cum_vol if cum_vol > 0 else 0
|
||||
std = variance ** 0.5
|
||||
|
||||
return {
|
||||
"vwap": vwap,
|
||||
"std": std,
|
||||
"upper1": vwap + std,
|
||||
"lower1": vwap - std,
|
||||
"upper2": vwap + 2 * std,
|
||||
"lower2": vwap - 2 * std,
|
||||
}
|
||||
|
||||
|
||||
def calc_rsi(closes, period=14):
|
||||
"""Calculate RSI."""
|
||||
if len(closes) < period + 1:
|
||||
return 50 # Default neutral
|
||||
|
||||
gains = []
|
||||
losses = []
|
||||
for i in range(1, len(closes)):
|
||||
diff = closes[i] - closes[i - 1]
|
||||
gains.append(max(diff, 0))
|
||||
losses.append(max(-diff, 0))
|
||||
|
||||
avg_gain = sum(gains[-period:]) / period
|
||||
avg_loss = sum(losses[-period:]) / period
|
||||
|
||||
if avg_loss == 0:
|
||||
return 100
|
||||
rs = avg_gain / avg_loss
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
|
||||
def analyze_coin(coin, klines=None):
|
||||
"""Full VWAP + RSI + momentum analysis for a single coin."""
|
||||
symbol = coin["symbol"].upper()
|
||||
price = coin["current_price"]
|
||||
|
||||
if not klines:
|
||||
klines = get_binance_klines(symbol, "1h", 48)
|
||||
|
||||
if not klines or len(klines) < 10:
|
||||
return None
|
||||
|
||||
# VWAP
|
||||
vwap_data = calc_vwap(klines)
|
||||
vwap = vwap_data["vwap"]
|
||||
vwap_diff = (price - vwap) / vwap * 100
|
||||
|
||||
# RSI
|
||||
closes = [k["c"] for k in klines]
|
||||
rsi = calc_rsi(closes)
|
||||
|
||||
# Volume trend
|
||||
recent_vol = sum(k["v"] for k in klines[-6:]) # Last 6h
|
||||
avg_vol = sum(k["v"] for k in klines) / (len(klines) / 6)
|
||||
vol_ratio = recent_vol / avg_vol if avg_vol > 0 else 1
|
||||
|
||||
# Momentum (last 4h vs prior 4h)
|
||||
if len(klines) >= 8:
|
||||
recent_mom = (klines[-1]["c"] - klines[-4]["c"]) / klines[-4]["c"] * 100
|
||||
prior_mom = (klines[-4]["c"] - klines[-8]["c"]) / klines[-8]["c"] * 100
|
||||
else:
|
||||
recent_mom = prior_mom = 0
|
||||
|
||||
# 24h change from CoinGecko
|
||||
change_24h = coin.get("price_change_percentage_24h", 0) or 0
|
||||
|
||||
# Score: -100 to +100
|
||||
score = 0
|
||||
signals = []
|
||||
|
||||
# VWAP position
|
||||
if vwap_diff < -2:
|
||||
score += 30
|
||||
signals.append(f"Deep below VWAP ({vwap_diff:.1f}%)")
|
||||
elif vwap_diff < -0.5:
|
||||
score += 15
|
||||
signals.append(f"Below VWAP ({vwap_diff:.1f}%)")
|
||||
elif vwap_diff > 2:
|
||||
score -= 20
|
||||
signals.append(f"Extended above VWAP ({vwap_diff:.1f}%)")
|
||||
elif vwap_diff > 0.5:
|
||||
score += 5
|
||||
signals.append(f"Above VWAP ({vwap_diff:.1f}%)")
|
||||
|
||||
# RSI
|
||||
if rsi < 30:
|
||||
score += 30
|
||||
signals.append(f"RSI oversold ({rsi:.0f})")
|
||||
elif rsi < 40:
|
||||
score += 15
|
||||
signals.append(f"RSI low ({rsi:.0f})")
|
||||
elif rsi > 70:
|
||||
score -= 25
|
||||
signals.append(f"RSI overbought ({rsi:.0f})")
|
||||
elif rsi > 60:
|
||||
score -= 10
|
||||
signals.append(f"RSI elevated ({rsi:.0f})")
|
||||
|
||||
# Volume confirmation
|
||||
if vol_ratio > 1.5:
|
||||
score += 10 if vwap_diff < 0 else -10 # High vol at support = bullish
|
||||
signals.append(f"High volume ({vol_ratio:.1f}x)")
|
||||
elif vol_ratio < 0.5:
|
||||
score -= 5
|
||||
signals.append(f"Low volume ({vol_ratio:.1f}x)")
|
||||
|
||||
# Momentum reversal
|
||||
if recent_mom > 0 and prior_mom < 0:
|
||||
score += 15
|
||||
signals.append("Momentum reversal (bullish)")
|
||||
elif recent_mom < 0 and prior_mom > 0:
|
||||
score -= 15
|
||||
signals.append("Momentum reversal (bearish)")
|
||||
|
||||
# 24h trend
|
||||
if change_24h < -5:
|
||||
score += 10 # Potential bounce
|
||||
signals.append(f"24h dump ({change_24h:.1f}%)")
|
||||
elif change_24h > 5:
|
||||
score -= 10 # Potential pullback
|
||||
signals.append(f"24h pump ({change_24h:.1f}%)")
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"name": coin["name"],
|
||||
"price": price,
|
||||
"market_cap_rank": coin.get("market_cap_rank", 0),
|
||||
"vwap": round(vwap, 6),
|
||||
"vwap_diff_pct": round(vwap_diff, 2),
|
||||
"rsi": round(rsi, 1),
|
||||
"vol_ratio": round(vol_ratio, 2),
|
||||
"momentum_4h": round(recent_mom, 2),
|
||||
"change_24h": round(change_24h, 2),
|
||||
"score": score,
|
||||
"signals": signals,
|
||||
"vwap_bands": {k: round(v, 6) for k, v in vwap_data.items()},
|
||||
}
|
||||
|
||||
|
||||
# ── Trading Logic ──
|
||||
|
||||
def should_buy(analysis):
|
||||
"""Determine if we should buy based on analysis score."""
|
||||
if not analysis:
|
||||
return False, ""
|
||||
|
||||
score = analysis["score"]
|
||||
rsi = analysis["rsi"]
|
||||
vwap_diff = analysis["vwap_diff_pct"]
|
||||
|
||||
# Strong buy: oversold + below VWAP
|
||||
if score >= 40:
|
||||
return True, f"Strong buy signal (score {score}): {', '.join(analysis['signals'])}"
|
||||
|
||||
# Moderate buy: decent score + below VWAP
|
||||
if score >= 20 and vwap_diff < -0.5:
|
||||
return True, f"Buy signal (score {score}): {', '.join(analysis['signals'])}"
|
||||
|
||||
return False, ""
|
||||
|
||||
|
||||
def should_sell(analysis, position):
|
||||
"""Determine if we should sell a position."""
|
||||
if not analysis:
|
||||
return False, 0, ""
|
||||
|
||||
price = analysis["price"]
|
||||
avg_price = position["avg_price"]
|
||||
pnl_pct = (price - avg_price) / avg_price * 100
|
||||
rsi = analysis["rsi"]
|
||||
vwap_diff = analysis["vwap_diff_pct"]
|
||||
score = analysis["score"]
|
||||
|
||||
# Take profit: +5% gain with overbought signals
|
||||
if pnl_pct >= 5 and (rsi > 65 or vwap_diff > 1):
|
||||
return True, 100, f"Take profit ({pnl_pct:.1f}%, RSI {rsi:.0f})"
|
||||
|
||||
# Strong take profit: +10% gain
|
||||
if pnl_pct >= 10:
|
||||
return True, 50, f"Partial take profit ({pnl_pct:.1f}%)"
|
||||
|
||||
# Stop loss: -8%
|
||||
if pnl_pct <= -8:
|
||||
return True, 100, f"Stop loss ({pnl_pct:.1f}%)"
|
||||
|
||||
# Bearish signals on losing position
|
||||
if pnl_pct < -3 and score <= -20:
|
||||
return True, 100, f"Cut loss (score {score}, PnL {pnl_pct:.1f}%)"
|
||||
|
||||
# Extended above VWAP with big gain
|
||||
if pnl_pct >= 3 and vwap_diff > 2:
|
||||
return True, 50, f"Extended above VWAP ({vwap_diff:.1f}%), lock gains"
|
||||
|
||||
return False, 0, ""
|
||||
|
||||
|
||||
# ── Main Scanner ──
|
||||
|
||||
def run_scan(max_positions=10, position_size=5000):
|
||||
"""
|
||||
Full scan: analyze top 150 cryptos, make buy/sell decisions.
|
||||
max_positions: max simultaneous positions
|
||||
position_size: USD per position
|
||||
"""
|
||||
print(f"=== Crypto Market Watch Scan ===")
|
||||
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
|
||||
print()
|
||||
|
||||
# Get top coins
|
||||
print("Fetching top 150 coins...", flush=True)
|
||||
coins = get_top_coins(150)
|
||||
# Deduplicate by symbol (CoinGecko sometimes returns dupes)
|
||||
seen_symbols = set()
|
||||
unique_coins = []
|
||||
for c in coins:
|
||||
sym = c["symbol"].upper()
|
||||
if sym not in seen_symbols:
|
||||
seen_symbols.add(sym)
|
||||
unique_coins.append(c)
|
||||
coins = unique_coins
|
||||
print(f"Got {len(coins)} unique coins")
|
||||
|
||||
# Map symbols to Binance format
|
||||
# Only analyze coins available on Binance US
|
||||
binance_symbols = set()
|
||||
try:
|
||||
url = "https://api.binance.us/api/v3/exchangeInfo"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
exchange = json.loads(resp.read())
|
||||
for s in exchange["symbols"]:
|
||||
if s["quoteAsset"] == "USDT" and s["status"] == "TRADING":
|
||||
binance_symbols.add(s["baseAsset"])
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Binance US has {len(binance_symbols)} USDT pairs")
|
||||
|
||||
# Analyze each coin
|
||||
analyses = []
|
||||
prices = {}
|
||||
|
||||
for coin in coins:
|
||||
symbol = coin["symbol"].upper()
|
||||
prices[symbol] = coin["current_price"]
|
||||
|
||||
if symbol not in binance_symbols:
|
||||
continue
|
||||
|
||||
analysis = analyze_coin(coin)
|
||||
if analysis:
|
||||
analyses.append(analysis)
|
||||
|
||||
time.sleep(0.2) # Rate limit
|
||||
|
||||
print(f"Analyzed {len(analyses)} coins with Binance data")
|
||||
|
||||
# Get current portfolio
|
||||
portfolio = get_portfolio()
|
||||
current_positions = set(portfolio["positions"].keys())
|
||||
|
||||
# ── SELL DECISIONS ──
|
||||
sells = []
|
||||
for symbol, pos in list(portfolio["positions"].items()):
|
||||
analysis = next((a for a in analyses if a["symbol"] == symbol), None)
|
||||
if not analysis:
|
||||
# Can't analyze — skip (don't sell blind)
|
||||
continue
|
||||
|
||||
do_sell, sell_pct, reason = should_sell(analysis, pos)
|
||||
if do_sell:
|
||||
trade, err = sell(symbol, analysis["price"], sell_pct, reason)
|
||||
if trade:
|
||||
sells.append(trade)
|
||||
print(f" 📤 SELL {symbol} @ ${analysis['price']:,.4f} ({sell_pct}%) — {reason}")
|
||||
|
||||
# ── BUY DECISIONS ──
|
||||
portfolio = get_portfolio() # Refresh after sells
|
||||
num_positions = len(portfolio["positions"])
|
||||
available_slots = max_positions - num_positions
|
||||
|
||||
# Sort by score (best opportunities first)
|
||||
buy_candidates = sorted(
|
||||
[a for a in analyses if a["symbol"] not in portfolio["positions"]],
|
||||
key=lambda x: -x["score"]
|
||||
)
|
||||
|
||||
buys = []
|
||||
for analysis in buy_candidates[:available_slots * 2]: # Check 2x candidates
|
||||
if len(buys) >= available_slots:
|
||||
break
|
||||
if portfolio["cash"] < position_size:
|
||||
break
|
||||
|
||||
do_buy, reason = should_buy(analysis)
|
||||
if do_buy:
|
||||
trade, err = buy(analysis["symbol"], analysis["price"], position_size, reason)
|
||||
if trade:
|
||||
buys.append(trade)
|
||||
portfolio = get_portfolio() # Refresh
|
||||
print(f" 📥 BUY {analysis['symbol']} @ ${analysis['price']:,.4f} — {reason}")
|
||||
|
||||
# Take snapshot
|
||||
snapshot = take_snapshot(prices)
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Buys: {len(buys)} | Sells: {len(sells)}")
|
||||
print(f"Portfolio: ${snapshot['total_value']:,.2f} ({snapshot['total_pnl_pct']:+.2f}%)")
|
||||
print(f"Cash: ${snapshot['cash']:,.2f} | Positions: {snapshot['num_positions']}")
|
||||
|
||||
if snapshot["positions"]:
|
||||
print(f"\nOpen Positions:")
|
||||
for p in snapshot["positions"]:
|
||||
emoji = "🟢" if p["pnl"] >= 0 else "🔴"
|
||||
print(f" {emoji} {p['symbol']}: ${p['value']:,.2f} ({p['pnl_pct']:+.1f}%)")
|
||||
|
||||
# Top opportunities not taken
|
||||
print(f"\nTop Scoring Coins:")
|
||||
for a in sorted(analyses, key=lambda x: -x["score"])[:10]:
|
||||
held = "📌" if a["symbol"] in portfolio.get("positions", {}) else " "
|
||||
print(f" {held} {a['symbol']:<8} score:{a['score']:>4} | RSI:{a['rsi']:.0f} | VWAP:{a['vwap_diff_pct']:+.1f}% | 24h:{a['change_24h']:+.1f}%")
|
||||
|
||||
return {"buys": buys, "sells": sells, "snapshot": snapshot, "analyses": analyses}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = run_scan()
|
||||
Reference in New Issue
Block a user