Add short signal scanner + leverage trading game engine + auto-trader
- short_scanner.py: RSI/VWAP/MACD/Bollinger-based short signal detection - leverage_game.py: Full game engine with longs/shorts/leverage/liquidations - leverage_trader.py: Auto-trader connecting scanners to game with TP/SL/trailing stops - Leverage Challenge game initialized: $10K, 20x max leverage, player 'case' - systemd timer: every 15min scan + trade - Telegram alerts on opens/closes/liquidations
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"game_id": "1ac7d29c",
|
||||||
|
"name": "Leverage Challenge",
|
||||||
|
"starting_cash": 10000.0,
|
||||||
|
"max_leverage": 20,
|
||||||
|
"funding_rate_8h": 0.01,
|
||||||
|
"maker_fee": 0.02,
|
||||||
|
"taker_fee": 0.05,
|
||||||
|
"start_date": "2026-02-09",
|
||||||
|
"creator": "case",
|
||||||
|
"created_at": "2026-02-10T02:31:27.614107+00:00",
|
||||||
|
"players": [
|
||||||
|
"case"
|
||||||
|
],
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"cash": 10000.0,
|
||||||
|
"positions": {},
|
||||||
|
"total_realized_pnl": 0,
|
||||||
|
"total_fees_paid": 0,
|
||||||
|
"total_funding_paid": 0
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"peak_pnl": {},
|
||||||
|
"last_alert": null
|
||||||
|
}
|
||||||
487
projects/crypto-signals/data/short-scanner/scan_log.json
Normal file
487
projects/crypto-signals/data/short-scanner/scan_log.json
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-10T02:31:38.063585+00:00",
|
||||||
|
"coins_scanned": 29,
|
||||||
|
"strong_signals": 0,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"symbol": "FIL",
|
||||||
|
"price": 0.954,
|
||||||
|
"rsi": 61.7,
|
||||||
|
"vwap_pct": 9.0,
|
||||||
|
"macd_histogram": -0.932576,
|
||||||
|
"bb_position": 0.76,
|
||||||
|
"change_24h": 0.74,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 0.05,
|
||||||
|
"score": 40,
|
||||||
|
"reasons": [
|
||||||
|
"RSI mildly elevated (61.7)",
|
||||||
|
"Well above VWAP (+9.0%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:34.839978+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "NEAR",
|
||||||
|
"price": 1.045,
|
||||||
|
"rsi": 60.0,
|
||||||
|
"vwap_pct": 1.62,
|
||||||
|
"macd_histogram": -1.030379,
|
||||||
|
"bb_position": 0.93,
|
||||||
|
"change_24h": -0.85,
|
||||||
|
"change_4h": 2.35,
|
||||||
|
"vol_trend": 1.02,
|
||||||
|
"score": 38,
|
||||||
|
"reasons": [
|
||||||
|
"RSI mildly elevated (60.0)",
|
||||||
|
"Slightly above VWAP (+1.6%)",
|
||||||
|
"MACD bearish + accelerating",
|
||||||
|
"Near upper Bollinger (0.93)"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:33.678025+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "OP",
|
||||||
|
"price": 0.19,
|
||||||
|
"rsi": 64.2,
|
||||||
|
"vwap_pct": 3.23,
|
||||||
|
"macd_histogram": -0.187435,
|
||||||
|
"bb_position": 0.79,
|
||||||
|
"change_24h": 0.53,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 1.07,
|
||||||
|
"score": 35,
|
||||||
|
"reasons": [
|
||||||
|
"RSI mildly elevated (64.2)",
|
||||||
|
"Above VWAP (+3.2%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:36.709597+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ARB",
|
||||||
|
"price": 0.114,
|
||||||
|
"rsi": 52.1,
|
||||||
|
"vwap_pct": 3.09,
|
||||||
|
"macd_histogram": -0.113948,
|
||||||
|
"bb_position": 0.72,
|
||||||
|
"change_24h": -3.55,
|
||||||
|
"change_4h": 2.06,
|
||||||
|
"vol_trend": 0.11,
|
||||||
|
"score": 30,
|
||||||
|
"reasons": [
|
||||||
|
"Above VWAP (+3.1%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:36.470618+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA",
|
||||||
|
"price": 0.2693,
|
||||||
|
"rsi": 50.2,
|
||||||
|
"vwap_pct": 1.35,
|
||||||
|
"macd_histogram": -0.269763,
|
||||||
|
"bb_position": 0.62,
|
||||||
|
"change_24h": -1.43,
|
||||||
|
"change_4h": -0.81,
|
||||||
|
"vol_trend": 0.15,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+1.3%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:32.520420+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "LINK",
|
||||||
|
"price": 8.88,
|
||||||
|
"rsi": 55.1,
|
||||||
|
"vwap_pct": 1.7,
|
||||||
|
"macd_histogram": -8.839752,
|
||||||
|
"bb_position": 0.73,
|
||||||
|
"change_24h": 0.79,
|
||||||
|
"change_4h": -0.34,
|
||||||
|
"vol_trend": 0.84,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+1.7%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:32.968544+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "UNI",
|
||||||
|
"price": 3.519,
|
||||||
|
"rsi": 56.2,
|
||||||
|
"vwap_pct": 2.62,
|
||||||
|
"macd_histogram": -3.494918,
|
||||||
|
"bb_position": 0.65,
|
||||||
|
"change_24h": 2.18,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 0.72,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+2.6%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:34.365690+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "AAVE",
|
||||||
|
"price": 113.55,
|
||||||
|
"rsi": 55.0,
|
||||||
|
"vwap_pct": 1.28,
|
||||||
|
"macd_histogram": -112.920164,
|
||||||
|
"bb_position": 0.71,
|
||||||
|
"change_24h": 0.82,
|
||||||
|
"change_4h": 2.23,
|
||||||
|
"vol_trend": 0.55,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+1.3%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:34.599689+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "APT",
|
||||||
|
"price": 1.067,
|
||||||
|
"rsi": 49.4,
|
||||||
|
"vwap_pct": 1.06,
|
||||||
|
"macd_histogram": -1.076147,
|
||||||
|
"bb_position": 0.63,
|
||||||
|
"change_24h": -2.65,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 0.23,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+1.1%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:35.993067+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "SUI",
|
||||||
|
"price": 0.9668,
|
||||||
|
"rsi": 50.4,
|
||||||
|
"vwap_pct": 1.54,
|
||||||
|
"macd_histogram": -0.969969,
|
||||||
|
"bb_position": 0.66,
|
||||||
|
"change_24h": -2.0,
|
||||||
|
"change_4h": -0.17,
|
||||||
|
"vol_trend": 0.03,
|
||||||
|
"score": 23,
|
||||||
|
"reasons": [
|
||||||
|
"Slightly above VWAP (+1.5%)",
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:36.231436+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "BTC",
|
||||||
|
"price": 70249.15,
|
||||||
|
"rsi": 51.0,
|
||||||
|
"vwap_pct": 0.34,
|
||||||
|
"macd_histogram": -70262.247326,
|
||||||
|
"bb_position": 0.63,
|
||||||
|
"change_24h": -1.38,
|
||||||
|
"change_4h": 0.18,
|
||||||
|
"vol_trend": 0.96,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:31.360741+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ETH",
|
||||||
|
"price": 2109.49,
|
||||||
|
"rsi": 55.4,
|
||||||
|
"vwap_pct": 0.97,
|
||||||
|
"macd_histogram": -2104.718634,
|
||||||
|
"bb_position": 0.67,
|
||||||
|
"change_24h": 0.44,
|
||||||
|
"change_4h": 0.2,
|
||||||
|
"vol_trend": 1.27,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:31.596017+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "SOL",
|
||||||
|
"price": 86.88,
|
||||||
|
"rsi": 52.0,
|
||||||
|
"vwap_pct": 0.65,
|
||||||
|
"macd_histogram": -86.940038,
|
||||||
|
"bb_position": 0.65,
|
||||||
|
"change_24h": -0.98,
|
||||||
|
"change_4h": 0.17,
|
||||||
|
"vol_trend": 0.88,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:31.806936+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "XRP",
|
||||||
|
"price": 1.4454,
|
||||||
|
"rsi": 53.8,
|
||||||
|
"vwap_pct": 0.52,
|
||||||
|
"macd_histogram": -1.439527,
|
||||||
|
"bb_position": 0.66,
|
||||||
|
"change_24h": -0.39,
|
||||||
|
"change_4h": 0.59,
|
||||||
|
"vol_trend": 1.67,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:32.046327+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "DOGE",
|
||||||
|
"price": 0.09629,
|
||||||
|
"rsi": 52.4,
|
||||||
|
"vwap_pct": 0.47,
|
||||||
|
"macd_histogram": -0.096217,
|
||||||
|
"bb_position": 0.7,
|
||||||
|
"change_24h": -0.98,
|
||||||
|
"change_4h": -0.43,
|
||||||
|
"vol_trend": 2.79,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:32.284061+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "AVAX",
|
||||||
|
"price": 9.01,
|
||||||
|
"rsi": 47.3,
|
||||||
|
"vwap_pct": 0.43,
|
||||||
|
"macd_histogram": -9.05205,
|
||||||
|
"bb_position": 0.56,
|
||||||
|
"change_24h": -1.31,
|
||||||
|
"change_4h": -0.44,
|
||||||
|
"vol_trend": 0.29,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:32.758345+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "DOT",
|
||||||
|
"price": 1.316,
|
||||||
|
"rsi": 47.5,
|
||||||
|
"vwap_pct": 0.69,
|
||||||
|
"macd_histogram": -1.325125,
|
||||||
|
"bb_position": 0.57,
|
||||||
|
"change_24h": -2.52,
|
||||||
|
"change_4h": -0.9,
|
||||||
|
"vol_trend": 0.23,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:33.205989+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "MATIC",
|
||||||
|
"price": 0.4492,
|
||||||
|
"rsi": 41.9,
|
||||||
|
"vwap_pct": -0.77,
|
||||||
|
"macd_histogram": -0.452491,
|
||||||
|
"bb_position": 0.32,
|
||||||
|
"change_24h": -1.58,
|
||||||
|
"change_4h": 0.22,
|
||||||
|
"vol_trend": 1.07,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:33.442040+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ATOM",
|
||||||
|
"price": 1.953,
|
||||||
|
"rsi": 49.3,
|
||||||
|
"vwap_pct": 0.52,
|
||||||
|
"macd_histogram": -1.965896,
|
||||||
|
"bb_position": 0.5,
|
||||||
|
"change_24h": 0.93,
|
||||||
|
"change_4h": 0.05,
|
||||||
|
"vol_trend": 1.04,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:33.888750+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "LTC",
|
||||||
|
"price": 54.36,
|
||||||
|
"rsi": 50.1,
|
||||||
|
"vwap_pct": 0.22,
|
||||||
|
"macd_histogram": -54.485415,
|
||||||
|
"bb_position": 0.61,
|
||||||
|
"change_24h": -1.06,
|
||||||
|
"change_4h": -0.22,
|
||||||
|
"vol_trend": 2.73,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:34.128167+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ALGO",
|
||||||
|
"price": 0.0951,
|
||||||
|
"rsi": 45.3,
|
||||||
|
"vwap_pct": -1.14,
|
||||||
|
"macd_histogram": -0.095952,
|
||||||
|
"bb_position": 0.5,
|
||||||
|
"change_24h": -2.56,
|
||||||
|
"change_4h": -2.36,
|
||||||
|
"vol_trend": 1.65,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:35.076205+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "XLM",
|
||||||
|
"price": 0.1614,
|
||||||
|
"rsi": 52.5,
|
||||||
|
"vwap_pct": 0.89,
|
||||||
|
"macd_histogram": -0.160963,
|
||||||
|
"bb_position": 0.69,
|
||||||
|
"change_24h": -1.34,
|
||||||
|
"change_4h": 1.13,
|
||||||
|
"vol_trend": 2.74,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:35.314005+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "VET",
|
||||||
|
"price": 0.00791,
|
||||||
|
"rsi": 48.8,
|
||||||
|
"vwap_pct": -0.02,
|
||||||
|
"macd_histogram": -0.007928,
|
||||||
|
"bb_position": 0.55,
|
||||||
|
"change_24h": -3.06,
|
||||||
|
"change_4h": 0.89,
|
||||||
|
"vol_trend": 0.27,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:35.550100+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ICP",
|
||||||
|
"price": 2.782,
|
||||||
|
"rsi": 40.1,
|
||||||
|
"vwap_pct": -1.9,
|
||||||
|
"macd_histogram": -2.843397,
|
||||||
|
"bb_position": 0.14,
|
||||||
|
"change_24h": 3.81,
|
||||||
|
"change_4h": -2.49,
|
||||||
|
"vol_trend": 10.38,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:35.759005+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "SEI",
|
||||||
|
"price": 0.075,
|
||||||
|
"rsi": 29.9,
|
||||||
|
"vwap_pct": -0.25,
|
||||||
|
"macd_histogram": -0.075645,
|
||||||
|
"bb_position": 0.38,
|
||||||
|
"change_24h": -4.34,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 0.11,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:36.945126+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "HYPE",
|
||||||
|
"price": 31.49,
|
||||||
|
"rsi": 45.7,
|
||||||
|
"vwap_pct": -1.11,
|
||||||
|
"macd_histogram": -31.681649,
|
||||||
|
"bb_position": 0.39,
|
||||||
|
"change_24h": -5.29,
|
||||||
|
"change_4h": 0.41,
|
||||||
|
"vol_trend": 21.91,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:37.184127+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "TRUMP",
|
||||||
|
"price": 3.446,
|
||||||
|
"rsi": 50.1,
|
||||||
|
"vwap_pct": 0.08,
|
||||||
|
"macd_histogram": -3.45085,
|
||||||
|
"bb_position": 0.49,
|
||||||
|
"change_24h": 0.35,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 0.81,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:37.440389+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "PUMP",
|
||||||
|
"price": 0.002041,
|
||||||
|
"rsi": 37.6,
|
||||||
|
"vwap_pct": -2.03,
|
||||||
|
"macd_histogram": -0.002063,
|
||||||
|
"bb_position": 0.31,
|
||||||
|
"change_24h": -4.31,
|
||||||
|
"change_4h": 0.0,
|
||||||
|
"vol_trend": 1.28,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:37.674937+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ASTER",
|
||||||
|
"price": 0.612,
|
||||||
|
"rsi": 50.2,
|
||||||
|
"vwap_pct": 0.21,
|
||||||
|
"macd_histogram": -0.610803,
|
||||||
|
"bb_position": 0.58,
|
||||||
|
"change_24h": -5.85,
|
||||||
|
"change_4h": 0.33,
|
||||||
|
"vol_trend": 0.63,
|
||||||
|
"score": 15,
|
||||||
|
"reasons": [
|
||||||
|
"MACD bearish + accelerating"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-02-10T02:31:37.912733+00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
504
projects/crypto-signals/leverage_game.py
Normal file
504
projects/crypto-signals/leverage_game.py
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Crypto Leverage Trading Game Engine
|
||||||
|
Paper trading with longs, shorts, and configurable leverage.
|
||||||
|
Tracks liquidation prices, unrealized PnL, and funding costs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, date, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent / "data" / "leverage-game"
|
||||||
|
GAMES_DIR = DATA_DIR / "games"
|
||||||
|
|
||||||
|
BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/price"
|
||||||
|
|
||||||
|
|
||||||
|
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.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(data, indent=2, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def _game_path(game_id):
|
||||||
|
return GAMES_DIR / game_id / "game.json"
|
||||||
|
|
||||||
|
def _player_path(game_id, username):
|
||||||
|
return GAMES_DIR / game_id / "players" / username / "portfolio.json"
|
||||||
|
|
||||||
|
def _trades_path(game_id, username):
|
||||||
|
return GAMES_DIR / game_id / "players" / username / "trades.json"
|
||||||
|
|
||||||
|
def _snapshots_path(game_id, username):
|
||||||
|
return GAMES_DIR / game_id / "players" / username / "snapshots.json"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Price Fetching ──
|
||||||
|
|
||||||
|
def get_price(symbol):
|
||||||
|
"""Get current price from Binance US."""
|
||||||
|
if not symbol.endswith("USDT"):
|
||||||
|
symbol = f"{symbol}USDT"
|
||||||
|
url = f"{BINANCE_TICKER}?symbol={symbol}"
|
||||||
|
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(req, timeout=10)
|
||||||
|
return float(json.loads(resp.read())['price'])
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Game Management ──
|
||||||
|
|
||||||
|
def create_game(name, starting_cash=10_000.0, max_leverage=20, creator="system"):
|
||||||
|
"""Create a new leverage trading game."""
|
||||||
|
game_id = str(uuid.uuid4())[:8]
|
||||||
|
config = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"name": name,
|
||||||
|
"starting_cash": starting_cash,
|
||||||
|
"max_leverage": max_leverage,
|
||||||
|
"funding_rate_8h": 0.01, # 0.01% per 8h (typical perp funding)
|
||||||
|
"maker_fee": 0.02, # 0.02%
|
||||||
|
"taker_fee": 0.05, # 0.05%
|
||||||
|
"start_date": date.today().isoformat(),
|
||||||
|
"creator": creator,
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"players": [],
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
_save(_game_path(game_id), config)
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
|
def list_games(active_only=True):
|
||||||
|
"""List all leverage games."""
|
||||||
|
games = []
|
||||||
|
if not GAMES_DIR.exists():
|
||||||
|
return games
|
||||||
|
for gid in os.listdir(GAMES_DIR):
|
||||||
|
gp = _game_path(gid)
|
||||||
|
if gp.exists():
|
||||||
|
config = _load(gp)
|
||||||
|
if active_only and config.get("status") != "active":
|
||||||
|
continue
|
||||||
|
games.append(config)
|
||||||
|
return sorted(games, key=lambda g: g.get("created_at", ""), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_game(game_id):
|
||||||
|
return _load(_game_path(game_id))
|
||||||
|
|
||||||
|
|
||||||
|
def join_game(game_id, username):
|
||||||
|
"""Add player to game."""
|
||||||
|
config = get_game(game_id)
|
||||||
|
if not config:
|
||||||
|
return {"error": "Game not found"}
|
||||||
|
if username in config["players"]:
|
||||||
|
return {"error": f"{username} already in game"}
|
||||||
|
|
||||||
|
config["players"].append(username)
|
||||||
|
_save(_game_path(game_id), config)
|
||||||
|
|
||||||
|
_save(_player_path(game_id, username), {
|
||||||
|
"cash": config["starting_cash"],
|
||||||
|
"positions": {},
|
||||||
|
"total_realized_pnl": 0,
|
||||||
|
"total_fees_paid": 0,
|
||||||
|
"total_funding_paid": 0,
|
||||||
|
})
|
||||||
|
_save(_trades_path(game_id, username), [])
|
||||||
|
_save(_snapshots_path(game_id, username), [])
|
||||||
|
|
||||||
|
return {"success": True, "game_id": game_id, "username": username}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Position Math ──
|
||||||
|
|
||||||
|
def calc_liquidation_price(entry_price, leverage, direction):
|
||||||
|
"""
|
||||||
|
Simplified liquidation price.
|
||||||
|
Long: liq = entry * (1 - 1/leverage)
|
||||||
|
Short: liq = entry * (1 + 1/leverage)
|
||||||
|
"""
|
||||||
|
if direction == "long":
|
||||||
|
return entry_price * (1 - 1 / leverage)
|
||||||
|
else: # short
|
||||||
|
return entry_price * (1 + 1 / leverage)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_unrealized_pnl(entry_price, current_price, size_usd, leverage, direction):
|
||||||
|
"""
|
||||||
|
Calculate unrealized PnL for a leveraged position.
|
||||||
|
size_usd = margin (collateral). Notional = size_usd * leverage.
|
||||||
|
"""
|
||||||
|
notional = size_usd * leverage
|
||||||
|
shares = notional / entry_price
|
||||||
|
|
||||||
|
if direction == "long":
|
||||||
|
pnl = (current_price - entry_price) * shares
|
||||||
|
else: # short
|
||||||
|
pnl = (entry_price - current_price) * shares
|
||||||
|
|
||||||
|
return pnl
|
||||||
|
|
||||||
|
|
||||||
|
def is_liquidated(entry_price, current_price, leverage, direction):
|
||||||
|
"""Check if position would be liquidated."""
|
||||||
|
liq_price = calc_liquidation_price(entry_price, leverage, direction)
|
||||||
|
if direction == "long":
|
||||||
|
return current_price <= liq_price
|
||||||
|
else:
|
||||||
|
return current_price >= liq_price
|
||||||
|
|
||||||
|
|
||||||
|
# ── Trading ──
|
||||||
|
|
||||||
|
def open_position(game_id, username, symbol, direction, margin_usd, leverage, reason="Manual"):
|
||||||
|
"""
|
||||||
|
Open a leveraged position.
|
||||||
|
margin_usd: collateral put up
|
||||||
|
leverage: multiplier (e.g., 10x)
|
||||||
|
direction: 'long' or 'short'
|
||||||
|
"""
|
||||||
|
pf = _load(_player_path(game_id, username))
|
||||||
|
game = get_game(game_id)
|
||||||
|
if not pf or not game:
|
||||||
|
return {"error": "Player or game not found"}
|
||||||
|
|
||||||
|
if direction not in ("long", "short"):
|
||||||
|
return {"error": "Direction must be 'long' or 'short'"}
|
||||||
|
if leverage > game.get("max_leverage", 20):
|
||||||
|
return {"error": f"Max leverage is {game['max_leverage']}x"}
|
||||||
|
if margin_usd > pf["cash"]:
|
||||||
|
return {"error": f"Insufficient cash. Need ${margin_usd:.2f}, have ${pf['cash']:.2f}"}
|
||||||
|
|
||||||
|
symbol = symbol.upper().replace("USDT", "")
|
||||||
|
price = get_price(symbol)
|
||||||
|
if not price:
|
||||||
|
return {"error": f"Could not fetch price for {symbol}"}
|
||||||
|
|
||||||
|
notional = margin_usd * leverage
|
||||||
|
fee = notional * game.get("taker_fee", 0.05) / 100
|
||||||
|
|
||||||
|
# Deduct margin + entry fee from cash
|
||||||
|
pf["cash"] -= (margin_usd + fee)
|
||||||
|
pf["total_fees_paid"] = pf.get("total_fees_paid", 0) + fee
|
||||||
|
|
||||||
|
pos_id = f"{symbol}_{direction}_{str(uuid.uuid4())[:4]}"
|
||||||
|
liq_price = calc_liquidation_price(price, leverage, direction)
|
||||||
|
|
||||||
|
pf["positions"][pos_id] = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"direction": direction,
|
||||||
|
"leverage": leverage,
|
||||||
|
"margin_usd": margin_usd,
|
||||||
|
"notional": round(notional, 2),
|
||||||
|
"entry_price": price,
|
||||||
|
"current_price": price,
|
||||||
|
"liquidation_price": round(liq_price, 4),
|
||||||
|
"unrealized_pnl": 0,
|
||||||
|
"entry_fee": round(fee, 4),
|
||||||
|
"opened_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
_save(_player_path(game_id, username), pf)
|
||||||
|
|
||||||
|
# Log trade
|
||||||
|
trades = _load(_trades_path(game_id, username), [])
|
||||||
|
trades.append({
|
||||||
|
"action": "OPEN",
|
||||||
|
"pos_id": pos_id,
|
||||||
|
"symbol": symbol,
|
||||||
|
"direction": direction,
|
||||||
|
"leverage": leverage,
|
||||||
|
"margin_usd": margin_usd,
|
||||||
|
"notional": round(notional, 2),
|
||||||
|
"entry_price": price,
|
||||||
|
"liquidation_price": round(liq_price, 4),
|
||||||
|
"fee": round(fee, 4),
|
||||||
|
"reason": reason,
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
})
|
||||||
|
_save(_trades_path(game_id, username), trades)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"pos_id": pos_id,
|
||||||
|
"symbol": symbol,
|
||||||
|
"direction": direction,
|
||||||
|
"leverage": leverage,
|
||||||
|
"entry_price": price,
|
||||||
|
"margin": margin_usd,
|
||||||
|
"notional": round(notional, 2),
|
||||||
|
"liquidation_price": round(liq_price, 4),
|
||||||
|
"fee": round(fee, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def close_position(game_id, username, pos_id, reason="Manual"):
|
||||||
|
"""Close a leveraged position."""
|
||||||
|
pf = _load(_player_path(game_id, username))
|
||||||
|
game = get_game(game_id)
|
||||||
|
if not pf or not game:
|
||||||
|
return {"error": "Player or game not found"}
|
||||||
|
if pos_id not in pf["positions"]:
|
||||||
|
return {"error": f"Position {pos_id} not found"}
|
||||||
|
|
||||||
|
pos = pf["positions"][pos_id]
|
||||||
|
price = get_price(pos["symbol"])
|
||||||
|
if not price:
|
||||||
|
return {"error": f"Could not fetch price for {pos['symbol']}"}
|
||||||
|
|
||||||
|
# Calculate PnL
|
||||||
|
pnl = calc_unrealized_pnl(
|
||||||
|
pos["entry_price"], price, pos["margin_usd"], pos["leverage"], pos["direction"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check liquidation
|
||||||
|
liquidated = is_liquidated(pos["entry_price"], price, pos["leverage"], pos["direction"])
|
||||||
|
if liquidated:
|
||||||
|
pnl = -pos["margin_usd"] # Lose entire margin
|
||||||
|
|
||||||
|
# Exit fee
|
||||||
|
notional = pos["margin_usd"] * pos["leverage"]
|
||||||
|
fee = notional * game.get("taker_fee", 0.05) / 100
|
||||||
|
|
||||||
|
# Return margin + PnL - fee to cash
|
||||||
|
returned = pos["margin_usd"] + pnl - fee
|
||||||
|
if returned < 0:
|
||||||
|
returned = 0 # Can't lose more than margin (no negative balance)
|
||||||
|
|
||||||
|
pf["cash"] += returned
|
||||||
|
pf["total_realized_pnl"] = pf.get("total_realized_pnl", 0) + pnl
|
||||||
|
pf["total_fees_paid"] = pf.get("total_fees_paid", 0) + fee
|
||||||
|
|
||||||
|
del pf["positions"][pos_id]
|
||||||
|
_save(_player_path(game_id, username), pf)
|
||||||
|
|
||||||
|
# Log trade
|
||||||
|
pnl_pct = (pnl / pos["margin_usd"] * 100) if pos["margin_usd"] > 0 else 0
|
||||||
|
trades = _load(_trades_path(game_id, username), [])
|
||||||
|
trades.append({
|
||||||
|
"action": "LIQUIDATED" if liquidated else "CLOSE",
|
||||||
|
"pos_id": pos_id,
|
||||||
|
"symbol": pos["symbol"],
|
||||||
|
"direction": pos["direction"],
|
||||||
|
"leverage": pos["leverage"],
|
||||||
|
"entry_price": pos["entry_price"],
|
||||||
|
"exit_price": price,
|
||||||
|
"margin_usd": pos["margin_usd"],
|
||||||
|
"pnl": round(pnl, 2),
|
||||||
|
"pnl_pct": round(pnl_pct, 2),
|
||||||
|
"fee": round(fee, 4),
|
||||||
|
"liquidated": liquidated,
|
||||||
|
"reason": reason,
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
})
|
||||||
|
_save(_trades_path(game_id, username), trades)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"pos_id": pos_id,
|
||||||
|
"symbol": pos["symbol"],
|
||||||
|
"direction": pos["direction"],
|
||||||
|
"entry_price": pos["entry_price"],
|
||||||
|
"exit_price": price,
|
||||||
|
"pnl": round(pnl, 2),
|
||||||
|
"pnl_pct": round(pnl_pct, 2),
|
||||||
|
"liquidated": liquidated,
|
||||||
|
"returned_to_cash": round(returned, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_prices(game_id, username):
|
||||||
|
"""Update all position prices and check for liquidations."""
|
||||||
|
pf = _load(_player_path(game_id, username))
|
||||||
|
if not pf:
|
||||||
|
return []
|
||||||
|
|
||||||
|
liquidations = []
|
||||||
|
to_liquidate = []
|
||||||
|
|
||||||
|
for pos_id, pos in pf["positions"].items():
|
||||||
|
price = get_price(pos["symbol"])
|
||||||
|
if not price:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pos["current_price"] = price
|
||||||
|
pos["unrealized_pnl"] = round(
|
||||||
|
calc_unrealized_pnl(pos["entry_price"], price, pos["margin_usd"], pos["leverage"], pos["direction"]),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_liquidated(pos["entry_price"], price, pos["leverage"], pos["direction"]):
|
||||||
|
to_liquidate.append(pos_id)
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
_save(_player_path(game_id, username), pf)
|
||||||
|
|
||||||
|
# Process liquidations
|
||||||
|
for pos_id in to_liquidate:
|
||||||
|
result = close_position(game_id, username, pos_id, reason="LIQUIDATED")
|
||||||
|
liquidations.append(result)
|
||||||
|
|
||||||
|
return liquidations
|
||||||
|
|
||||||
|
|
||||||
|
# ── Portfolio View ──
|
||||||
|
|
||||||
|
def get_portfolio(game_id, username):
|
||||||
|
"""Get full portfolio with live PnL."""
|
||||||
|
pf = _load(_player_path(game_id, username))
|
||||||
|
game = get_game(game_id)
|
||||||
|
if not pf or not game:
|
||||||
|
return None
|
||||||
|
|
||||||
|
starting = game["starting_cash"]
|
||||||
|
total_unrealized = sum(p.get("unrealized_pnl", 0) for p in pf["positions"].values())
|
||||||
|
total_margin_locked = sum(p["margin_usd"] for p in pf["positions"].values())
|
||||||
|
equity = pf["cash"] + total_margin_locked + total_unrealized
|
||||||
|
total_pnl = equity - starting
|
||||||
|
|
||||||
|
return {
|
||||||
|
"username": username,
|
||||||
|
"game_id": game_id,
|
||||||
|
"cash": round(pf["cash"], 2),
|
||||||
|
"margin_locked": round(total_margin_locked, 2),
|
||||||
|
"unrealized_pnl": round(total_unrealized, 2),
|
||||||
|
"realized_pnl": round(pf.get("total_realized_pnl", 0), 2),
|
||||||
|
"total_fees": round(pf.get("total_fees_paid", 0), 2),
|
||||||
|
"equity": round(equity, 2),
|
||||||
|
"total_pnl": round(total_pnl, 2),
|
||||||
|
"pnl_pct": round(total_pnl / starting * 100, 2),
|
||||||
|
"num_positions": len(pf["positions"]),
|
||||||
|
"positions": pf["positions"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_trades(game_id, username):
|
||||||
|
return _load(_trades_path(game_id, username), [])
|
||||||
|
|
||||||
|
|
||||||
|
def daily_snapshot(game_id, username):
|
||||||
|
"""Take daily snapshot."""
|
||||||
|
p = get_portfolio(game_id, username)
|
||||||
|
if not p:
|
||||||
|
return None
|
||||||
|
snapshots = _load(_snapshots_path(game_id, username), [])
|
||||||
|
today = date.today().isoformat()
|
||||||
|
snapshots = [s for s in snapshots if s["date"] != today]
|
||||||
|
snapshots.append({
|
||||||
|
"date": today,
|
||||||
|
"equity": p["equity"],
|
||||||
|
"total_pnl": p["total_pnl"],
|
||||||
|
"pnl_pct": p["pnl_pct"],
|
||||||
|
"cash": p["cash"],
|
||||||
|
"num_positions": p["num_positions"],
|
||||||
|
"realized_pnl": p["realized_pnl"],
|
||||||
|
"total_fees": p["total_fees"],
|
||||||
|
})
|
||||||
|
_save(_snapshots_path(game_id, username), snapshots)
|
||||||
|
return snapshots[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_leaderboard(game_id):
|
||||||
|
"""Leaderboard sorted by equity."""
|
||||||
|
game = get_game(game_id)
|
||||||
|
if not game:
|
||||||
|
return []
|
||||||
|
board = []
|
||||||
|
for username in game["players"]:
|
||||||
|
p = get_portfolio(game_id, username)
|
||||||
|
if p:
|
||||||
|
trades = get_trades(game_id, username)
|
||||||
|
closed = [t for t in trades if t.get("action") in ("CLOSE", "LIQUIDATED")]
|
||||||
|
wins = [t for t in closed if t.get("pnl", 0) > 0]
|
||||||
|
liquidations = [t for t in closed if t.get("liquidated")]
|
||||||
|
board.append({
|
||||||
|
"username": username,
|
||||||
|
"equity": p["equity"],
|
||||||
|
"total_pnl": p["total_pnl"],
|
||||||
|
"pnl_pct": p["pnl_pct"],
|
||||||
|
"num_positions": p["num_positions"],
|
||||||
|
"trades_closed": len(closed),
|
||||||
|
"win_rate": round(len(wins) / len(closed) * 100, 1) if closed else 0,
|
||||||
|
"liquidations": len(liquidations),
|
||||||
|
"total_fees": p["total_fees"],
|
||||||
|
})
|
||||||
|
return sorted(board, key=lambda x: x["pnl_pct"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Auto-Trader (Scanner Integration) ──
|
||||||
|
|
||||||
|
def auto_trade_from_scanner(game_id, username, scan_results, margin_per_trade=200, leverage=10):
|
||||||
|
"""
|
||||||
|
Automatically open positions based on scanner results.
|
||||||
|
Short scanner (score >= 50) → open short
|
||||||
|
Spot scanner (score >= 40) → open long
|
||||||
|
"""
|
||||||
|
opened = []
|
||||||
|
for r in scan_results:
|
||||||
|
symbol = r["symbol"]
|
||||||
|
score = r["score"]
|
||||||
|
|
||||||
|
# Determine direction based on which scanner produced this
|
||||||
|
direction = r.get("direction", "short") # Default to short for short scanner
|
||||||
|
|
||||||
|
if score < 40:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Scale leverage with conviction
|
||||||
|
if score >= 70:
|
||||||
|
lev = min(leverage, 15)
|
||||||
|
elif score >= 50:
|
||||||
|
lev = min(leverage, 10)
|
||||||
|
else:
|
||||||
|
lev = min(leverage, 5)
|
||||||
|
|
||||||
|
result = open_position(game_id, username, symbol, direction, margin_per_trade, lev,
|
||||||
|
reason=f"Scanner score:{score} | {', '.join(r.get('reasons', []))}")
|
||||||
|
if result.get("success"):
|
||||||
|
opened.append(result)
|
||||||
|
|
||||||
|
return opened
|
||||||
|
|
||||||
|
|
||||||
|
# ── Initialize ──
|
||||||
|
|
||||||
|
def ensure_default_game():
|
||||||
|
"""Create default Leverage Challenge game."""
|
||||||
|
for g in list_games():
|
||||||
|
if g["name"] == "Leverage Challenge":
|
||||||
|
return g["game_id"]
|
||||||
|
|
||||||
|
game_id = create_game("Leverage Challenge", starting_cash=10_000.0, max_leverage=20, creator="case")
|
||||||
|
join_game(game_id, "case")
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
game_id = ensure_default_game()
|
||||||
|
game = get_game(game_id)
|
||||||
|
print(f"Game: {game['name']} ({game_id})")
|
||||||
|
print(f"Players: {game['players']}")
|
||||||
|
print(f"Starting cash: ${game['starting_cash']:,.2f}")
|
||||||
|
print(f"Max leverage: {game['max_leverage']}x")
|
||||||
|
|
||||||
|
board = get_leaderboard(game_id)
|
||||||
|
for entry in board:
|
||||||
|
print(f" {entry['username']}: ${entry['equity']:,.2f} ({entry['pnl_pct']:+.2f}%) "
|
||||||
|
f"| {entry['trades_closed']} trades | {entry['win_rate']}% win | {entry['liquidations']} liquidated")
|
||||||
292
projects/crypto-signals/scripts/leverage_trader.py
Normal file
292
projects/crypto-signals/scripts/leverage_trader.py
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Automated Leverage Trader
|
||||||
|
Runs short scanner + spot scanner, opens positions in the Leverage Challenge game,
|
||||||
|
manages exits (TP/SL/trailing stop), and reports via Telegram.
|
||||||
|
|
||||||
|
Zero AI tokens — systemd timer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add parent to path for imports
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from leverage_game import (
|
||||||
|
ensure_default_game, get_game, get_portfolio, open_position,
|
||||||
|
close_position, update_prices, get_trades, get_leaderboard
|
||||||
|
)
|
||||||
|
from scripts.short_scanner import scan_coin, COINS as SHORT_COINS
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent.parent / "data" / "leverage-game"
|
||||||
|
STATE_FILE = DATA_DIR / "trader_state.json"
|
||||||
|
|
||||||
|
# Trading params
|
||||||
|
MARGIN_PER_TRADE = 200 # $200 margin per position
|
||||||
|
DEFAULT_LEVERAGE = 10 # 10x default
|
||||||
|
MAX_OPEN_POSITIONS = 10 # Max simultaneous positions
|
||||||
|
SHORT_SCORE_THRESHOLD = 50 # Min score to open short
|
||||||
|
LONG_SCORE_THRESHOLD = 45 # Min score to open long
|
||||||
|
TP_PCT = 5.0 # Take profit at 5% on margin (50% on notional at 10x)
|
||||||
|
SL_PCT = -3.0 # Stop loss at -3% on margin (30% on notional at 10x)
|
||||||
|
TRAILING_STOP_PCT = 2.0 # Trailing stop: close if drops 2% from peak
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
return json.loads(STATE_FILE.read_text())
|
||||||
|
return {"peak_pnl": {}, "last_alert": None}
|
||||||
|
|
||||||
|
def save_state(state):
|
||||||
|
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def send_telegram(message):
|
||||||
|
if not TELEGRAM_BOT_TOKEN:
|
||||||
|
print(f"[TG] {message}")
|
||||||
|
return
|
||||||
|
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||||
|
data = json.dumps({
|
||||||
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "HTML"
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, headers={
|
||||||
|
"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Telegram failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_short_scan():
|
||||||
|
"""Run short scanner on all coins."""
|
||||||
|
results = []
|
||||||
|
for symbol in SHORT_COINS:
|
||||||
|
r = scan_coin(symbol)
|
||||||
|
if r:
|
||||||
|
r["direction"] = "short"
|
||||||
|
results.append(r)
|
||||||
|
time.sleep(0.15)
|
||||||
|
return sorted(results, key=lambda x: x['score'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_spot_scan():
|
||||||
|
"""Run spot/long scanner (inverse of short criteria — oversold = buy)."""
|
||||||
|
results = []
|
||||||
|
for symbol in SHORT_COINS:
|
||||||
|
r = scan_coin(symbol)
|
||||||
|
if r:
|
||||||
|
# Invert: low RSI + below VWAP = long opportunity
|
||||||
|
long_score = 0
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
if r['rsi'] <= 25:
|
||||||
|
long_score += 30
|
||||||
|
reasons.append(f"RSI extremely oversold ({r['rsi']})")
|
||||||
|
elif r['rsi'] <= 30:
|
||||||
|
long_score += 25
|
||||||
|
reasons.append(f"RSI oversold ({r['rsi']})")
|
||||||
|
elif r['rsi'] <= 35:
|
||||||
|
long_score += 15
|
||||||
|
reasons.append(f"RSI low ({r['rsi']})")
|
||||||
|
elif r['rsi'] <= 40:
|
||||||
|
long_score += 5
|
||||||
|
reasons.append(f"RSI mildly low ({r['rsi']})")
|
||||||
|
|
||||||
|
if r['vwap_pct'] < -5:
|
||||||
|
long_score += 20
|
||||||
|
reasons.append(f"Well below VWAP ({r['vwap_pct']:+.1f}%)")
|
||||||
|
elif r['vwap_pct'] < -3:
|
||||||
|
long_score += 15
|
||||||
|
reasons.append(f"Below VWAP ({r['vwap_pct']:+.1f}%)")
|
||||||
|
elif r['vwap_pct'] < -1:
|
||||||
|
long_score += 8
|
||||||
|
reasons.append(f"Slightly below VWAP ({r['vwap_pct']:+.1f}%)")
|
||||||
|
|
||||||
|
if r['change_24h'] < -15:
|
||||||
|
long_score += 15
|
||||||
|
reasons.append(f"Dumped {r['change_24h']:.1f}% 24h")
|
||||||
|
elif r['change_24h'] < -8:
|
||||||
|
long_score += 10
|
||||||
|
reasons.append(f"Down {r['change_24h']:.1f}% 24h")
|
||||||
|
elif r['change_24h'] < -4:
|
||||||
|
long_score += 5
|
||||||
|
reasons.append(f"Down {r['change_24h']:.1f}% 24h")
|
||||||
|
|
||||||
|
if r['bb_position'] < 0:
|
||||||
|
long_score += 15
|
||||||
|
reasons.append(f"Below lower Bollinger ({r['bb_position']:.2f})")
|
||||||
|
elif r['bb_position'] < 0.15:
|
||||||
|
long_score += 10
|
||||||
|
reasons.append(f"Near lower Bollinger ({r['bb_position']:.2f})")
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"symbol": r["symbol"],
|
||||||
|
"price": r["price"],
|
||||||
|
"rsi": r["rsi"],
|
||||||
|
"vwap_pct": r["vwap_pct"],
|
||||||
|
"change_24h": r["change_24h"],
|
||||||
|
"bb_position": r["bb_position"],
|
||||||
|
"score": long_score,
|
||||||
|
"reasons": reasons,
|
||||||
|
"direction": "long",
|
||||||
|
})
|
||||||
|
time.sleep(0.15)
|
||||||
|
return sorted(results, key=lambda x: x['score'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def manage_exits(game_id, username, state):
|
||||||
|
"""Check open positions for TP/SL/trailing stop exits."""
|
||||||
|
pf = get_portfolio(game_id, username)
|
||||||
|
if not pf:
|
||||||
|
return []
|
||||||
|
|
||||||
|
exits = []
|
||||||
|
for pos_id, pos in list(pf["positions"].items()):
|
||||||
|
pnl_pct = (pos.get("unrealized_pnl", 0) / pos["margin_usd"] * 100) if pos["margin_usd"] > 0 else 0
|
||||||
|
|
||||||
|
# Track peak PnL for trailing stop
|
||||||
|
peak_key = pos_id
|
||||||
|
if peak_key not in state.get("peak_pnl", {}):
|
||||||
|
state["peak_pnl"][peak_key] = pnl_pct
|
||||||
|
if pnl_pct > state["peak_pnl"].get(peak_key, 0):
|
||||||
|
state["peak_pnl"][peak_key] = pnl_pct
|
||||||
|
|
||||||
|
peak = state["peak_pnl"].get(peak_key, 0)
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
# Take profit
|
||||||
|
if pnl_pct >= TP_PCT:
|
||||||
|
reason = f"TP hit ({pnl_pct:+.1f}%)"
|
||||||
|
# Stop loss
|
||||||
|
elif pnl_pct <= SL_PCT:
|
||||||
|
reason = f"SL hit ({pnl_pct:+.1f}%)"
|
||||||
|
# Trailing stop (only if we were profitable)
|
||||||
|
elif peak >= 2.0 and (peak - pnl_pct) >= TRAILING_STOP_PCT:
|
||||||
|
reason = f"Trailing stop (peak {peak:+.1f}%, now {pnl_pct:+.1f}%)"
|
||||||
|
|
||||||
|
if reason:
|
||||||
|
result = close_position(game_id, username, pos_id, reason=reason)
|
||||||
|
if result.get("success"):
|
||||||
|
exits.append(result)
|
||||||
|
# Clean up peak tracking
|
||||||
|
state["peak_pnl"].pop(peak_key, None)
|
||||||
|
|
||||||
|
return exits
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
game_id = ensure_default_game()
|
||||||
|
state = load_state()
|
||||||
|
|
||||||
|
print(f"=== Leverage Trader ===")
|
||||||
|
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
|
||||||
|
print(f"Game: {game_id}")
|
||||||
|
|
||||||
|
# 1. Update prices and check liquidations
|
||||||
|
liquidations = update_prices(game_id, "case")
|
||||||
|
for liq in liquidations:
|
||||||
|
msg = f"💀 <b>LIQUIDATED</b>: {liq['symbol']} {liq['direction']} {liq.get('leverage', '?')}x | Lost ${abs(liq.get('pnl', 0)):.2f}"
|
||||||
|
send_telegram(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
# 2. Manage exits (TP/SL/trailing)
|
||||||
|
exits = manage_exits(game_id, "case", state)
|
||||||
|
for ex in exits:
|
||||||
|
emoji = "✅" if ex.get("pnl", 0) > 0 else "❌"
|
||||||
|
msg = (f"{emoji} <b>Closed</b>: {ex['symbol']} {ex['direction']} | "
|
||||||
|
f"Entry: ${ex['entry_price']:.4f} → Exit: ${ex['exit_price']:.4f} | "
|
||||||
|
f"PnL: ${ex['pnl']:+.2f} ({ex['pnl_pct']:+.1f}%)")
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
# 3. Get current portfolio
|
||||||
|
pf = get_portfolio(game_id, "case")
|
||||||
|
num_open = pf["num_positions"] if pf else 0
|
||||||
|
slots = MAX_OPEN_POSITIONS - num_open
|
||||||
|
|
||||||
|
print(f"\nPortfolio: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%) | {num_open} positions | {slots} slots open")
|
||||||
|
|
||||||
|
# 4. Scan for new opportunities
|
||||||
|
if slots > 0:
|
||||||
|
# Run both scanners
|
||||||
|
shorts = run_short_scan()
|
||||||
|
longs = run_spot_scan()
|
||||||
|
|
||||||
|
# Get existing symbols to avoid doubling up
|
||||||
|
existing_symbols = set()
|
||||||
|
if pf:
|
||||||
|
for pos in pf["positions"].values():
|
||||||
|
existing_symbols.add(pos["symbol"])
|
||||||
|
|
||||||
|
opened = []
|
||||||
|
|
||||||
|
# Open short positions
|
||||||
|
for r in shorts:
|
||||||
|
if slots <= 0:
|
||||||
|
break
|
||||||
|
if r["score"] < SHORT_SCORE_THRESHOLD:
|
||||||
|
break
|
||||||
|
if r["symbol"] in existing_symbols:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7
|
||||||
|
result = open_position(game_id, "case", r["symbol"], "short", MARGIN_PER_TRADE, lev,
|
||||||
|
reason=f"Short scanner score:{r['score']}")
|
||||||
|
if result.get("success"):
|
||||||
|
opened.append(result)
|
||||||
|
existing_symbols.add(r["symbol"])
|
||||||
|
slots -= 1
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Open long positions
|
||||||
|
for r in longs:
|
||||||
|
if slots <= 0:
|
||||||
|
break
|
||||||
|
if r["score"] < LONG_SCORE_THRESHOLD:
|
||||||
|
break
|
||||||
|
if r["symbol"] in existing_symbols:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7
|
||||||
|
result = open_position(game_id, "case", r["symbol"], "long", MARGIN_PER_TRADE, lev,
|
||||||
|
reason=f"Long scanner score:{r['score']}")
|
||||||
|
if result.get("success"):
|
||||||
|
opened.append(result)
|
||||||
|
existing_symbols.add(r["symbol"])
|
||||||
|
slots -= 1
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
if opened:
|
||||||
|
lines = [f"📊 <b>Opened {len(opened)} positions</b>\n"]
|
||||||
|
for o in opened:
|
||||||
|
lines.append(f"{'🔴' if o['direction']=='short' else '🟢'} {o['symbol']} {o['direction']} {o['leverage']}x @ ${o['entry_price']:.4f} (${o['margin']:.0f} margin)")
|
||||||
|
send_telegram("\n".join(lines))
|
||||||
|
print(f"\nOpened {len(opened)} new positions")
|
||||||
|
|
||||||
|
# 5. Send periodic summary (every 4 hours)
|
||||||
|
if exits or liquidations:
|
||||||
|
pf = get_portfolio(game_id, "case") # Refresh
|
||||||
|
msg = (f"📈 <b>Leverage Challenge Update</b>\n"
|
||||||
|
f"Equity: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%)\n"
|
||||||
|
f"Positions: {pf['num_positions']} | Cash: ${pf['cash']:,.2f}\n"
|
||||||
|
f"Realized PnL: ${pf['realized_pnl']:+,.2f} | Fees: ${pf['total_fees']:,.2f}")
|
||||||
|
send_telegram(msg)
|
||||||
|
|
||||||
|
save_state(state)
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
336
projects/crypto-signals/scripts/short_scanner.py
Normal file
336
projects/crypto-signals/scripts/short_scanner.py
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Crypto Short Signal Scanner
|
||||||
|
Scans for overbought coins ripe for shorting.
|
||||||
|
Criteria: high RSI, price above VWAP, fading momentum, bearish divergence.
|
||||||
|
|
||||||
|
Zero AI tokens — runs as pure Python via systemd timer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Config
|
||||||
|
DATA_DIR = Path(__file__).parent.parent / "data" / "short-scanner"
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
SCAN_LOG = DATA_DIR / "scan_log.json"
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
|
||||||
|
|
||||||
|
BINANCE_KLINES = "https://api.binance.us/api/v3/klines"
|
||||||
|
BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/24hr"
|
||||||
|
|
||||||
|
# Coins to scan (popular leveraged trading coins)
|
||||||
|
COINS = [
|
||||||
|
"BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "DOGEUSDT",
|
||||||
|
"ADAUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT", "MATICUSDT",
|
||||||
|
"NEARUSDT", "ATOMUSDT", "LTCUSDT", "UNIUSDT", "AAVEUSDT",
|
||||||
|
"FILUSDT", "ALGOUSDT", "XLMUSDT", "VETUSDT", "ICPUSDT",
|
||||||
|
"APTUSDT", "SUIUSDT", "ARBUSDT", "OPUSDT", "SEIUSDT",
|
||||||
|
"HYPEUSDT", "TRUMPUSDT", "PUMPUSDT", "ASTERUSDT",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_klines(symbol, interval='1h', limit=100):
|
||||||
|
"""Fetch klines from Binance US."""
|
||||||
|
url = f"{BINANCE_KLINES}?symbol={symbol}&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 [{
|
||||||
|
'open': float(k[1]),
|
||||||
|
'high': float(k[2]),
|
||||||
|
'low': float(k[3]),
|
||||||
|
'close': float(k[4]),
|
||||||
|
'volume': float(k[5]),
|
||||||
|
'close_time': k[6],
|
||||||
|
} for k in raw]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def calc_rsi(closes, period=14):
|
||||||
|
"""Calculate RSI."""
|
||||||
|
if len(closes) < period + 1:
|
||||||
|
return 50
|
||||||
|
deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))]
|
||||||
|
gains = [d if d > 0 else 0 for d in deltas]
|
||||||
|
losses = [-d if d < 0 else 0 for d in deltas]
|
||||||
|
|
||||||
|
avg_gain = sum(gains[:period]) / period
|
||||||
|
avg_loss = sum(losses[:period]) / period
|
||||||
|
|
||||||
|
for i in range(period, len(deltas)):
|
||||||
|
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
|
||||||
|
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
|
||||||
|
|
||||||
|
if avg_loss == 0:
|
||||||
|
return 100
|
||||||
|
rs = avg_gain / avg_loss
|
||||||
|
return round(100 - (100 / (1 + rs)), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_vwap(klines):
|
||||||
|
"""Calculate VWAP from klines."""
|
||||||
|
cum_vol = 0
|
||||||
|
cum_tp_vol = 0
|
||||||
|
for k in klines:
|
||||||
|
tp = (k['high'] + k['low'] + k['close']) / 3
|
||||||
|
cum_vol += k['volume']
|
||||||
|
cum_tp_vol += tp * k['volume']
|
||||||
|
if cum_vol == 0:
|
||||||
|
return 0
|
||||||
|
return cum_tp_vol / cum_vol
|
||||||
|
|
||||||
|
|
||||||
|
def calc_ema(values, period):
|
||||||
|
"""Calculate EMA."""
|
||||||
|
if not values:
|
||||||
|
return 0
|
||||||
|
multiplier = 2 / (period + 1)
|
||||||
|
ema = values[0]
|
||||||
|
for v in values[1:]:
|
||||||
|
ema = (v - ema) * multiplier + ema
|
||||||
|
return ema
|
||||||
|
|
||||||
|
|
||||||
|
def calc_macd(closes):
|
||||||
|
"""Calculate MACD (12, 26, 9)."""
|
||||||
|
if len(closes) < 26:
|
||||||
|
return 0, 0, 0
|
||||||
|
ema12 = calc_ema(closes, 12)
|
||||||
|
ema26 = calc_ema(closes, 26)
|
||||||
|
macd_line = ema12 - ema26
|
||||||
|
# Approximate signal line
|
||||||
|
signal = calc_ema(closes[-9:], 9) if len(closes) >= 9 else macd_line
|
||||||
|
histogram = macd_line - signal
|
||||||
|
return macd_line, signal, histogram
|
||||||
|
|
||||||
|
|
||||||
|
def calc_bollinger_position(closes, period=20):
|
||||||
|
"""How far price is from upper Bollinger band. >1 = above upper band."""
|
||||||
|
if len(closes) < period:
|
||||||
|
return 0.5
|
||||||
|
recent = closes[-period:]
|
||||||
|
sma = sum(recent) / period
|
||||||
|
std = (sum((x - sma)**2 for x in recent) / period) ** 0.5
|
||||||
|
if std == 0:
|
||||||
|
return 0.5
|
||||||
|
upper = sma + 2 * std
|
||||||
|
lower = sma - 2 * std
|
||||||
|
band_width = upper - lower
|
||||||
|
if band_width == 0:
|
||||||
|
return 0.5
|
||||||
|
return (closes[-1] - lower) / band_width
|
||||||
|
|
||||||
|
|
||||||
|
def volume_trend(klines, lookback=10):
|
||||||
|
"""Compare recent volume to average. >1 means increasing volume."""
|
||||||
|
if len(klines) < lookback * 2:
|
||||||
|
return 1.0
|
||||||
|
recent_vol = sum(k['volume'] for k in klines[-lookback:]) / lookback
|
||||||
|
older_vol = sum(k['volume'] for k in klines[-lookback*2:-lookback]) / lookback
|
||||||
|
if older_vol == 0:
|
||||||
|
return 1.0
|
||||||
|
return recent_vol / older_vol
|
||||||
|
|
||||||
|
|
||||||
|
def scan_coin(symbol):
|
||||||
|
"""Analyze a single coin for short signals."""
|
||||||
|
# Get 1h candles for RSI/VWAP/indicators
|
||||||
|
klines_1h = get_klines(symbol, '1h', 100)
|
||||||
|
if len(klines_1h) < 30:
|
||||||
|
return None
|
||||||
|
|
||||||
|
closes = [k['close'] for k in klines_1h]
|
||||||
|
current_price = closes[-1]
|
||||||
|
|
||||||
|
# RSI (14-period on 1h)
|
||||||
|
rsi = calc_rsi(closes)
|
||||||
|
|
||||||
|
# VWAP (24h)
|
||||||
|
vwap_24h = calc_vwap(klines_1h[-24:])
|
||||||
|
vwap_pct = ((current_price - vwap_24h) / vwap_24h * 100) if vwap_24h else 0
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd_line, signal_line, histogram = calc_macd(closes)
|
||||||
|
macd_bearish = histogram < 0 # Below signal = bearish
|
||||||
|
|
||||||
|
# Bollinger position
|
||||||
|
bb_pos = calc_bollinger_position(closes)
|
||||||
|
|
||||||
|
# Volume trend
|
||||||
|
vol_trend = volume_trend(klines_1h)
|
||||||
|
|
||||||
|
# 24h change
|
||||||
|
price_24h_ago = closes[-24] if len(closes) >= 24 else closes[0]
|
||||||
|
change_24h = ((current_price - price_24h_ago) / price_24h_ago * 100) if price_24h_ago else 0
|
||||||
|
|
||||||
|
# 4h change (momentum)
|
||||||
|
price_4h_ago = closes[-4] if len(closes) >= 4 else closes[0]
|
||||||
|
change_4h = ((current_price - price_4h_ago) / price_4h_ago * 100) if price_4h_ago else 0
|
||||||
|
|
||||||
|
# === SHORT SCORING ===
|
||||||
|
score = 0
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
# RSI overbought (max 30 pts)
|
||||||
|
if rsi >= 80:
|
||||||
|
score += 30
|
||||||
|
reasons.append(f"RSI extremely overbought ({rsi})")
|
||||||
|
elif rsi >= 70:
|
||||||
|
score += 25
|
||||||
|
reasons.append(f"RSI overbought ({rsi})")
|
||||||
|
elif rsi >= 65:
|
||||||
|
score += 15
|
||||||
|
reasons.append(f"RSI elevated ({rsi})")
|
||||||
|
elif rsi >= 60:
|
||||||
|
score += 5
|
||||||
|
reasons.append(f"RSI mildly elevated ({rsi})")
|
||||||
|
|
||||||
|
# Price above VWAP (max 20 pts)
|
||||||
|
if vwap_pct > 5:
|
||||||
|
score += 20
|
||||||
|
reasons.append(f"Well above VWAP (+{vwap_pct:.1f}%)")
|
||||||
|
elif vwap_pct > 3:
|
||||||
|
score += 15
|
||||||
|
reasons.append(f"Above VWAP (+{vwap_pct:.1f}%)")
|
||||||
|
elif vwap_pct > 1:
|
||||||
|
score += 8
|
||||||
|
reasons.append(f"Slightly above VWAP (+{vwap_pct:.1f}%)")
|
||||||
|
|
||||||
|
# MACD bearish crossover (max 15 pts)
|
||||||
|
if macd_bearish and histogram < -0.001 * current_price:
|
||||||
|
score += 15
|
||||||
|
reasons.append("MACD bearish + accelerating")
|
||||||
|
elif macd_bearish:
|
||||||
|
score += 10
|
||||||
|
reasons.append("MACD bearish crossover")
|
||||||
|
|
||||||
|
# Bollinger band position (max 15 pts)
|
||||||
|
if bb_pos > 1.0:
|
||||||
|
score += 15
|
||||||
|
reasons.append(f"Above upper Bollinger ({bb_pos:.2f})")
|
||||||
|
elif bb_pos > 0.85:
|
||||||
|
score += 10
|
||||||
|
reasons.append(f"Near upper Bollinger ({bb_pos:.2f})")
|
||||||
|
|
||||||
|
# Big recent pump (mean reversion candidate) (max 15 pts)
|
||||||
|
if change_24h > 15:
|
||||||
|
score += 15
|
||||||
|
reasons.append(f"Pumped +{change_24h:.1f}% 24h")
|
||||||
|
elif change_24h > 8:
|
||||||
|
score += 10
|
||||||
|
reasons.append(f"Up +{change_24h:.1f}% 24h")
|
||||||
|
elif change_24h > 4:
|
||||||
|
score += 5
|
||||||
|
reasons.append(f"Up +{change_24h:.1f}% 24h")
|
||||||
|
|
||||||
|
# Volume fading on uptrend (exhaustion) (5 pts)
|
||||||
|
if change_24h > 2 and vol_trend < 0.7:
|
||||||
|
score += 5
|
||||||
|
reasons.append("Volume fading on uptrend (exhaustion)")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"symbol": symbol.replace("USDT", ""),
|
||||||
|
"price": current_price,
|
||||||
|
"rsi": rsi,
|
||||||
|
"vwap_pct": round(vwap_pct, 2),
|
||||||
|
"macd_histogram": round(histogram, 6),
|
||||||
|
"bb_position": round(bb_pos, 2),
|
||||||
|
"change_24h": round(change_24h, 2),
|
||||||
|
"change_4h": round(change_4h, 2),
|
||||||
|
"vol_trend": round(vol_trend, 2),
|
||||||
|
"score": score,
|
||||||
|
"reasons": reasons,
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_telegram_alert(message):
|
||||||
|
"""Send alert via Telegram bot API."""
|
||||||
|
if not TELEGRAM_BOT_TOKEN:
|
||||||
|
print(f"[ALERT] {message}")
|
||||||
|
return
|
||||||
|
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||||
|
data = json.dumps({
|
||||||
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "HTML"
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Mozilla/5.0"
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Telegram alert failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"=== Crypto Short Signal Scanner ===")
|
||||||
|
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for symbol in COINS:
|
||||||
|
result = scan_coin(symbol)
|
||||||
|
if result:
|
||||||
|
results.append(result)
|
||||||
|
time.sleep(0.15) # Rate limiting
|
||||||
|
|
||||||
|
# Sort by score descending
|
||||||
|
results.sort(key=lambda x: x['score'], reverse=True)
|
||||||
|
|
||||||
|
# Print all results
|
||||||
|
for r in results:
|
||||||
|
emoji = "🔴" if r['score'] >= 50 else "🟡" if r['score'] >= 30 else "⚪"
|
||||||
|
print(f"{emoji} {r['symbol']:8s} score:{r['score']:3d} | RSI:{r['rsi']:5.1f} | VWAP:{r['vwap_pct']:+6.1f}% | 24h:{r['change_24h']:+6.1f}% | BB:{r['bb_position']:.2f}")
|
||||||
|
if r['reasons']:
|
||||||
|
for reason in r['reasons']:
|
||||||
|
print(f" → {reason}")
|
||||||
|
|
||||||
|
# Alert on strong short signals (score >= 50)
|
||||||
|
strong = [r for r in results if r['score'] >= 50]
|
||||||
|
if strong:
|
||||||
|
lines = ["🔴 <b>Short Signals Detected</b>\n"]
|
||||||
|
for r in strong:
|
||||||
|
lines.append(f"<b>{r['symbol']}</b> (score: {r['score']})")
|
||||||
|
lines.append(f" Price: ${r['price']:.4f} | RSI: {r['rsi']} | VWAP: {r['vwap_pct']:+.1f}%")
|
||||||
|
lines.append(f" 24h: {r['change_24h']:+.1f}% | BB: {r['bb_position']:.2f}")
|
||||||
|
for reason in r['reasons']:
|
||||||
|
lines.append(f" → {reason}")
|
||||||
|
lines.append("")
|
||||||
|
send_telegram_alert("\n".join(lines))
|
||||||
|
|
||||||
|
# Save scan log
|
||||||
|
log = []
|
||||||
|
if SCAN_LOG.exists():
|
||||||
|
try:
|
||||||
|
log = json.loads(SCAN_LOG.read_text())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.append({
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"coins_scanned": len(results),
|
||||||
|
"strong_signals": len(strong),
|
||||||
|
"results": results,
|
||||||
|
})
|
||||||
|
log = log[-500:]
|
||||||
|
SCAN_LOG.write_text(json.dumps(log, indent=2))
|
||||||
|
|
||||||
|
print(f"\n📊 Summary: {len(results)} scanned, {len(strong)} strong short signals")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"last_check": "2026-02-10T02:13:59.845747+00:00",
|
"last_check": "2026-02-10T02:31:59.875011+00:00",
|
||||||
"total_tracked": 3100,
|
"total_tracked": 3100,
|
||||||
"new_this_check": 0
|
"new_this_check": 0
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user