Full sync - all projects, memory, configs

This commit is contained in:
2026-03-21 20:27:59 -05:00
parent 2447677d4a
commit b33de10902
395 changed files with 1635300 additions and 459211 deletions

11
projects/crypto-signals/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
node_modules/
.next/
__pycache__/
*.pyc
.env
*.db
*.sqlite
*.log
venv/
dist/
.credentials/

View File

@ -0,0 +1,433 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CoinEx Futures Scanner</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0e17; color: #e1e5eb; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
.header { background: #111827; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #1f2937; }
.header h1 { font-size: 16px; color: #60a5fa; }
.header .status { font-size: 11px; color: #6b7280; }
.header .status .live { color: #10b981; }
.controls { background: #111827; padding: 8px 20px; display: flex; gap: 12px; align-items: center; border-bottom: 1px solid #1f2937; }
.controls label { color: #9ca3af; font-size: 11px; }
.controls select, .controls input { background: #1f2937; border: 1px solid #374151; color: #e5e7eb; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-family: inherit; }
.thresholds { background: #0d1321; padding: 10px 20px; display: flex; gap: 20px; border-bottom: 1px solid #1f2937; font-size: 11px; }
.thresholds .th-item { display: flex; gap: 6px; align-items: center; }
.thresholds .th-label { color: #6b7280; }
.thresholds .th-long { color: #10b981; }
.thresholds .th-short { color: #ef4444; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1px; background: #1f2937; padding: 1px; }
.coin-card { background: #111827; padding: 12px; }
.coin-card.signal-long { border-left: 3px solid #10b981; }
.coin-card.signal-short { border-left: 3px solid #ef4444; }
.coin-card.signal-none { border-left: 3px solid #374151; }
.coin-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.coin-name { font-size: 14px; font-weight: 700; }
.coin-price { font-size: 13px; color: #9ca3af; }
.coin-change { font-size: 12px; padding: 2px 6px; border-radius: 3px; }
.coin-change.up { background: #064e3b; color: #34d399; }
.coin-change.down { background: #7f1d1d; color: #fca5a5; }
.indicators { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 8px; }
.ind { background: #0a0e17; padding: 6px 8px; border-radius: 4px; }
.ind-label { font-size: 10px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
.ind-value { font-size: 14px; font-weight: 600; margin-top: 2px; }
.ind-pts { font-size: 10px; margin-top: 1px; }
.ind-pts.pts-high { color: #10b981; }
.ind-pts.pts-med { color: #f59e0b; }
.ind-pts.pts-low { color: #6b7280; }
.score-bar { margin-top: 8px; }
.score-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.score-label { font-size: 11px; color: #9ca3af; }
.score-value { font-size: 13px; font-weight: 700; }
.score-value.above { color: #10b981; }
.score-value.below { color: #6b7280; }
.bar-bg { background: #1f2937; height: 6px; border-radius: 3px; overflow: hidden; position: relative; }
.bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
.bar-fill.long { background: linear-gradient(90deg, #065f46, #10b981); }
.bar-fill.short { background: linear-gradient(90deg, #7f1d1d, #ef4444); }
.bar-fill.neutral { background: #374151; }
.bar-threshold { position: absolute; top: -2px; height: 10px; width: 2px; background: #f59e0b; }
.signal-badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: 700; margin-top: 6px; }
.signal-badge.long { background: #064e3b; color: #34d399; }
.signal-badge.short { background: #7f1d1d; color: #fca5a5; }
.signal-badge.neutral { background: #1f2937; color: #6b7280; }
.position-tag { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 700; margin-left: 6px; }
.position-tag.live-long { background: #10b981; color: #000; }
.position-tag.live-short { background: #ef4444; color: #fff; }
.rsi-gauge { position: relative; height: 8px; background: linear-gradient(90deg, #10b981 0%, #10b981 30%, #f59e0b 30%, #f59e0b 70%, #ef4444 70%, #ef4444 100%); border-radius: 4px; margin-top: 4px; }
.rsi-needle { position: absolute; top: -3px; width: 4px; height: 14px; background: #fff; border-radius: 2px; transform: translateX(-50%); transition: left 0.5s; }
.loading { text-align: center; padding: 60px; color: #6b7280; }
.error { text-align: center; padding: 60px; color: #ef4444; }
.summary { background: #111827; padding: 10px 20px; border-bottom: 1px solid #1f2937; display: flex; gap: 20px; font-size: 12px; }
.summary .s-item { display: flex; gap: 4px; }
.summary .s-label { color: #6b7280; }
.summary .s-val { font-weight: 600; }
.summary .s-val.green { color: #10b981; }
.summary .s-val.red { color: #ef4444; }
.summary .s-val.yellow { color: #f59e0b; }
</style>
</head>
<body>
<div class="header">
<h1>⚡ CoinEx Futures Scanner</h1>
<div class="status">
<span class="live">● LIVE</span> |
Last scan: <span id="lastScan"></span> |
Auto-refresh: <span id="refreshCountdown">30</span>s
</div>
</div>
<div class="thresholds">
<div class="th-item"><span class="th-label">LONG threshold:</span> <span class="th-long">45 pts</span></div>
<div class="th-item"><span class="th-label">SHORT threshold:</span> <span class="th-short">50 pts</span></div>
<div class="th-item"><span class="th-label">TP:</span> <span style="color:#10b981">+5%</span></div>
<div class="th-item"><span class="th-label">SL:</span> <span style="color:#ef4444">-3%</span></div>
<div class="th-item"><span class="th-label">Leverage:</span> <span style="color:#60a5fa">5x / 7x (score≥60)</span></div>
</div>
<div class="controls">
<label>Sort:</label>
<select id="sortBy" onchange="renderCoins()">
<option value="longScore">Long Score ↓</option>
<option value="shortScore">Short Score ↓</option>
<option value="name">Name A-Z</option>
<option value="change">24h Change</option>
<option value="rsi">RSI</option>
</select>
<label>Filter:</label>
<select id="filterBy" onchange="renderCoins()">
<option value="all">All Coins</option>
<option value="longSignal">Long Signals Only</option>
<option value="shortSignal">Short Signals Only</option>
<option value="anySignal">Any Signal</option>
</select>
</div>
<div class="summary" id="summary"></div>
<div class="grid" id="grid"><div class="loading">Scanning 29 coins...</div></div>
<script>
const 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"
];
const LONG_THRESHOLD = 45;
const SHORT_THRESHOLD = 50;
let coinData = [];
let refreshTimer = 30;
function calcRSI(closes, period=14) {
if (closes.length < period + 1) return 50;
let gains = [], losses = [];
for (let i = 1; i < closes.length; i++) {
let d = closes[i] - closes[i-1];
gains.push(d > 0 ? d : 0);
losses.push(d < 0 ? -d : 0);
}
let avgGain = gains.slice(0, period).reduce((a,b)=>a+b,0) / period;
let avgLoss = losses.slice(0, period).reduce((a,b)=>a+b,0) / period;
for (let i = period; i < gains.length; i++) {
avgGain = (avgGain * (period-1) + gains[i]) / period;
avgLoss = (avgLoss * (period-1) + losses[i]) / period;
}
if (avgLoss === 0) return 100;
return Math.round((100 - 100/(1 + avgGain/avgLoss)) * 10) / 10;
}
function calcVWAP(klines) {
let cumVP = 0, cumVol = 0;
for (let k of klines) {
let typical = (k.high + k.low + k.close) / 3;
cumVP += typical * k.volume;
cumVol += k.volume;
}
return cumVol > 0 ? cumVP / cumVol : klines[klines.length-1].close;
}
function calcBB(closes, period=20) {
if (closes.length < period) return { lower: closes[closes.length-1], mid: closes[closes.length-1], upper: closes[closes.length-1] };
let slice = closes.slice(-period);
let sma = slice.reduce((a,b)=>a+b,0) / period;
let std = Math.sqrt(slice.reduce((a,b)=>a+(b-sma)**2,0) / period);
return { lower: sma - 2*std, mid: sma, upper: sma + 2*std };
}
function scoreLong(rsi, vwapPct, change24h, bbPos) {
let score = 0, breakdown = {};
if (rsi < 25) { score += 30; breakdown.rsi = 30; }
else if (rsi < 30) { score += 25; breakdown.rsi = 25; }
else if (rsi < 35) { score += 15; breakdown.rsi = 15; }
else if (rsi < 40) { score += 5; breakdown.rsi = 5; }
else { breakdown.rsi = 0; }
if (vwapPct < -3) { score += 20; breakdown.vwap = 20; }
else if (vwapPct < -1.5) { score += 15; breakdown.vwap = 15; }
else if (vwapPct < 0) { score += 8; breakdown.vwap = 8; }
else { breakdown.vwap = 0; }
if (change24h < -10) { score += 15; breakdown.change = 15; }
else if (change24h < -5) { score += 10; breakdown.change = 10; }
else if (change24h < -2) { score += 5; breakdown.change = 5; }
else { breakdown.change = 0; }
if (bbPos < 0) { score += 15; breakdown.bb = 15; }
else if (bbPos < 0.2) { score += 10; breakdown.bb = 10; }
else { breakdown.bb = 0; }
breakdown.total = score;
return breakdown;
}
function scoreShort(rsi, vwapPct, change24h, bbPos) {
let score = 0, breakdown = {};
if (rsi > 75) { score += 30; breakdown.rsi = 30; }
else if (rsi > 70) { score += 25; breakdown.rsi = 25; }
else if (rsi > 65) { score += 15; breakdown.rsi = 15; }
else if (rsi > 60) { score += 5; breakdown.rsi = 5; }
else { breakdown.rsi = 0; }
if (vwapPct > 3) { score += 20; breakdown.vwap = 20; }
else if (vwapPct > 1.5) { score += 15; breakdown.vwap = 15; }
else if (vwapPct > 0) { score += 8; breakdown.vwap = 8; }
else { breakdown.vwap = 0; }
if (change24h > 10) { score += 15; breakdown.change = 15; }
else if (change24h > 5) { score += 10; breakdown.change = 10; }
else if (change24h > 2) { score += 5; breakdown.change = 5; }
else { breakdown.change = 0; }
if (bbPos > 1) { score += 15; breakdown.bb = 15; }
else if (bbPos > 0.8) { score += 10; breakdown.bb = 10; }
else { breakdown.bb = 0; }
breakdown.total = score;
return breakdown;
}
async function fetchCoin(symbol) {
try {
let url = `https://api.binance.us/api/v3/klines?symbol=${symbol}&interval=1h&limit=100`;
let resp = await fetch(url);
let raw = await resp.json();
if (!Array.isArray(raw) || raw.length < 30) return null;
let klines = raw.map(k => ({
open: parseFloat(k[1]),
high: parseFloat(k[2]),
low: parseFloat(k[3]),
close: parseFloat(k[4]),
volume: parseFloat(k[5])
}));
let closes = klines.map(k => k.close);
let price = closes[closes.length - 1];
let price24hAgo = closes[Math.max(0, closes.length - 25)];
let change24h = ((price - price24hAgo) / price24hAgo) * 100;
let rsi = calcRSI(closes);
let vwap = calcVWAP(klines.slice(-24));
let vwapPct = ((price - vwap) / vwap) * 100;
let bb = calcBB(closes);
let bbRange = bb.upper - bb.lower || 1;
let bbPos = (price - bb.lower) / bbRange;
let longBreakdown = scoreLong(rsi, vwapPct, change24h, bbPos);
let shortBreakdown = scoreShort(rsi, vwapPct, change24h, bbPos);
return {
symbol,
name: symbol.replace('USDT',''),
price,
change24h,
rsi,
vwap,
vwapPct,
bbPos,
bbLower: bb.lower,
bbMid: bb.mid,
bbUpper: bb.upper,
longScore: longBreakdown,
shortScore: shortBreakdown,
isLongSignal: longBreakdown.total >= LONG_THRESHOLD,
isShortSignal: shortBreakdown.total >= SHORT_THRESHOLD,
leverage: Math.max(longBreakdown.total, shortBreakdown.total) >= 60 ? '7x' : '5x'
};
} catch(e) {
return null;
}
}
function ptsClass(pts, max) {
if (pts >= max * 0.6) return 'pts-high';
if (pts >= max * 0.3) return 'pts-med';
return 'pts-low';
}
function formatPrice(p) {
if (p >= 100) return p.toFixed(2);
if (p >= 1) return p.toFixed(4);
if (p >= 0.01) return p.toFixed(5);
return p.toFixed(7);
}
function renderCoins() {
let sorted = [...coinData];
let sortBy = document.getElementById('sortBy').value;
let filterBy = document.getElementById('filterBy').value;
if (filterBy === 'longSignal') sorted = sorted.filter(c => c.isLongSignal);
else if (filterBy === 'shortSignal') sorted = sorted.filter(c => c.isShortSignal);
else if (filterBy === 'anySignal') sorted = sorted.filter(c => c.isLongSignal || c.isShortSignal);
if (sortBy === 'longScore') sorted.sort((a,b) => b.longScore.total - a.longScore.total);
else if (sortBy === 'shortScore') sorted.sort((a,b) => b.shortScore.total - a.shortScore.total);
else if (sortBy === 'name') sorted.sort((a,b) => a.name.localeCompare(b.name));
else if (sortBy === 'change') sorted.sort((a,b) => a.change24h - b.change24h);
else if (sortBy === 'rsi') sorted.sort((a,b) => a.rsi - b.rsi);
let longSignals = coinData.filter(c => c.isLongSignal).length;
let shortSignals = coinData.filter(c => c.isShortSignal).length;
let avgRSI = (coinData.reduce((a,c) => a + c.rsi, 0) / coinData.length).toFixed(1);
document.getElementById('summary').innerHTML = `
<div class="s-item"><span class="s-label">Coins:</span> <span class="s-val">${coinData.length}</span></div>
<div class="s-item"><span class="s-label">Long signals:</span> <span class="s-val green">${longSignals}</span></div>
<div class="s-item"><span class="s-label">Short signals:</span> <span class="s-val red">${shortSignals}</span></div>
<div class="s-item"><span class="s-label">Avg RSI:</span> <span class="s-val ${avgRSI < 40 ? 'green' : avgRSI > 60 ? 'red' : 'yellow'}">${avgRSI}</span></div>
`;
let html = '';
for (let c of sorted) {
let signalClass = c.isLongSignal ? 'signal-long' : c.isShortSignal ? 'signal-short' : 'signal-none';
let changeClass = c.change24h >= 0 ? 'up' : 'down';
let bestScore = Math.max(c.longScore.total, c.shortScore.total);
let bestDir = c.longScore.total >= c.shortScore.total ? 'long' : 'short';
html += `
<div class="coin-card ${signalClass}">
<div class="coin-top">
<div>
<span class="coin-name">${c.name}</span>
${c.isLongSignal ? '<span class="position-tag live-long">LONG ✓</span>' : ''}
${c.isShortSignal ? '<span class="position-tag live-short">SHORT ✓</span>' : ''}
</div>
<span class="coin-change ${changeClass}">${c.change24h >= 0 ? '+' : ''}${c.change24h.toFixed(2)}%</span>
</div>
<div class="coin-price">$${formatPrice(c.price)}</div>
<div class="indicators">
<div class="ind">
<div class="ind-label">RSI (14)</div>
<div class="ind-value" style="color:${c.rsi < 30 ? '#10b981' : c.rsi > 70 ? '#ef4444' : '#f59e0b'}">${c.rsi}</div>
<div class="rsi-gauge"><div class="rsi-needle" style="left:${Math.min(100, Math.max(0, c.rsi))}%"></div></div>
<div class="ind-pts ${ptsClass(Math.max(c.longScore.rsi, c.shortScore.rsi), 30)}">
L:+${c.longScore.rsi} / S:+${c.shortScore.rsi}
</div>
</div>
<div class="ind">
<div class="ind-label">VWAP</div>
<div class="ind-value" style="color:${c.vwapPct < -1.5 ? '#10b981' : c.vwapPct > 1.5 ? '#ef4444' : '#9ca3af'}">${c.vwapPct >= 0 ? '+' : ''}${c.vwapPct.toFixed(2)}%</div>
<div class="ind-pts ${ptsClass(Math.max(c.longScore.vwap, c.shortScore.vwap), 20)}">
L:+${c.longScore.vwap} / S:+${c.shortScore.vwap}
</div>
</div>
<div class="ind">
<div class="ind-label">24h Change</div>
<div class="ind-value" style="color:${c.change24h < -5 ? '#10b981' : c.change24h > 5 ? '#ef4444' : '#9ca3af'}">${c.change24h >= 0 ? '+' : ''}${c.change24h.toFixed(2)}%</div>
<div class="ind-pts ${ptsClass(Math.max(c.longScore.change, c.shortScore.change), 15)}">
L:+${c.longScore.change} / S:+${c.shortScore.change}
</div>
</div>
<div class="ind">
<div class="ind-label">BB Position</div>
<div class="ind-value" style="color:${c.bbPos < 0.2 ? '#10b981' : c.bbPos > 0.8 ? '#ef4444' : '#9ca3af'}">${c.bbPos.toFixed(3)}</div>
<div class="ind-pts ${ptsClass(Math.max(c.longScore.bb, c.shortScore.bb), 15)}">
L:+${c.longScore.bb} / S:+${c.shortScore.bb}
</div>
</div>
</div>
<div class="score-bar">
<div class="score-row">
<span class="score-label">LONG</span>
<span class="score-value ${c.longScore.total >= LONG_THRESHOLD ? 'above' : 'below'}">${c.longScore.total}/80</span>
</div>
<div class="bar-bg">
<div class="bar-fill ${c.longScore.total >= LONG_THRESHOLD ? 'long' : 'neutral'}" style="width:${(c.longScore.total/80)*100}%"></div>
<div class="bar-threshold" style="left:${(LONG_THRESHOLD/80)*100}%"></div>
</div>
</div>
<div class="score-bar" style="margin-top:4px">
<div class="score-row">
<span class="score-label">SHORT</span>
<span class="score-value ${c.shortScore.total >= SHORT_THRESHOLD ? 'above' : 'below'}">${c.shortScore.total}/80</span>
</div>
<div class="bar-bg">
<div class="bar-fill ${c.shortScore.total >= SHORT_THRESHOLD ? 'short' : 'neutral'}" style="width:${(c.shortScore.total/80)*100}%"></div>
<div class="bar-threshold" style="left:${(SHORT_THRESHOLD/80)*100}%"></div>
</div>
</div>
${bestScore >= Math.min(LONG_THRESHOLD, SHORT_THRESHOLD) ?
`<span class="signal-badge ${bestDir}">${bestDir.toUpperCase()} ${bestScore}pts → ${c.leverage}</span>` :
`<span class="signal-badge neutral">No signal</span>`}
</div>`;
}
document.getElementById('grid').innerHTML = html || '<div class="loading">No coins match filter</div>';
}
async function scanAll() {
document.getElementById('grid').innerHTML = '<div class="loading">Scanning 29 coins...</div>';
// Fetch in batches of 5 to avoid rate limits
coinData = [];
for (let i = 0; i < COINS.length; i += 5) {
let batch = COINS.slice(i, i+5);
let results = await Promise.all(batch.map(fetchCoin));
coinData.push(...results.filter(r => r !== null));
renderCoins();
if (i + 5 < COINS.length) await new Promise(r => setTimeout(r, 500));
}
document.getElementById('lastScan').textContent = new Date().toLocaleTimeString();
renderCoins();
}
// Auto-refresh countdown
setInterval(() => {
refreshTimer--;
document.getElementById('refreshCountdown').textContent = refreshTimer;
if (refreshTimer <= 0) {
refreshTimer = 30;
scanAll();
}
}, 1000);
scanAll();
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""Simple HTTP server for the CoinEx Futures Scanner dashboard."""
import http.server
import os
import socketserver
PORT = 8891
DIR = os.path.dirname(os.path.abspath(__file__))
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIR, **kwargs)
def log_message(self, format, *args):
pass # Quiet
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"CoinEx Scanner dashboard at http://localhost:{PORT}")
httpd.serve_forever()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
{
"mode": "dry-run",
"position_size_pct": 5,
"max_positions": 2,
"max_leverage": 10,
"kill_switch_drawdown_pct": 50,
"tp_pct": 5,
"sl_pct": -3,
"trailing_stop_pct": 2,
"circuit_breaker_threshold": 3,
"scan_interval_minutes": 5,
"long_threshold": 45,
"short_threshold": 50,
"coin_blacklist": [],
"coin_whitelist": [],
"leverage_tiers": [
{
"min_score": 45,
"leverage": 5
},
{
"min_score": 60,
"leverage": 7
}
],
"scan_interval_seconds": 30,
"coin_watchlist": [
"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"
],
"last_updated": "2026-03-02T02:38:26.430Z"
}

View File

@ -0,0 +1,8 @@
{
"peak_pnl": {
"TRUMPUSDT_short": 0.88,
"PUMPUSDT_short": -2.7966
},
"last_alert": null,
"starting_balance": 146.83821322
}

View File

@ -1,78 +1,50 @@
{
"cash": 10548.59340884497,
"cash": 14573.327156868609,
"positions": {
"SEI_long_f875": {
"symbol": "SEI",
"direction": "long",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.0702,
"current_price": 0.0702,
"liquidation_price": 0.0655,
"unrealized_pnl": 0.0,
"entry_fee": 1.5,
"opened_at": "2026-02-11T15:15:18.869862+00:00",
"reason": "Long scanner score:70"
},
"OP_long_133e": {
"symbol": "OP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.172,
"current_price": 0.172,
"liquidation_price": 0.1474,
"unrealized_pnl": 0.0,
"entry_fee": 0.7,
"opened_at": "2026-02-12T03:15:17.913748+00:00",
"reason": "Long scanner score:50"
},
"ARB_short_f35e": {
"symbol": "ARB",
"PUMP_short_cb54": {
"symbol": "PUMP",
"direction": "short",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.1107,
"current_price": 0.1107,
"liquidation_price": 0.1265,
"entry_price": 0.002103,
"current_price": 0.002103,
"liquidation_price": 0.0024,
"unrealized_pnl": 0.0,
"entry_fee": 0.7,
"opened_at": "2026-02-12T04:15:18.517576+00:00",
"opened_at": "2026-02-20T03:31:00.752399+00:00",
"reason": "Short scanner score:58"
},
"FIL_short_9d66": {
"symbol": "FIL",
"direction": "short",
"ARB_long_0f74": {
"symbol": "ARB",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.93,
"current_price": 0.93,
"liquidation_price": 1.0629,
"entry_price": 0.0981,
"current_price": 0.0981,
"liquidation_price": 0.0841,
"unrealized_pnl": 0.0,
"entry_fee": 0.7,
"opened_at": "2026-02-12T06:00:17.937727+00:00",
"reason": "Short scanner score:50"
"opened_at": "2026-02-20T03:31:01.354884+00:00",
"reason": "Long scanner score:55"
},
"VET_short_1776": {
"symbol": "VET",
"direction": "short",
"FIL_long_0c5f": {
"symbol": "FIL",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.00785,
"current_price": 0.00785,
"liquidation_price": 0.009,
"entry_price": 0.9,
"current_price": 0.9,
"liquidation_price": 0.7714,
"unrealized_pnl": 0,
"entry_fee": 0.7,
"opened_at": "2026-02-12T06:15:18.429636+00:00",
"reason": "Short scanner score:55"
"opened_at": "2026-02-20T05:31:00.537230+00:00",
"reason": "Long scanner score:53"
}
},
"total_realized_pnl": 1820.693408845038,
"total_fees_paid": 272.0999999999996,
"total_realized_pnl": 6925.227156868812,
"total_fees_paid": 1751.9000000000133,
"total_funding_paid": 0
}

View File

@ -1,9 +1,7 @@
{
"peak_pnl": {
"SEI_long_f875": 0.0,
"OP_long_133e": 0.0,
"ARB_short_f35e": 0.0,
"FIL_short_9d66": 0.0
"PUMP_short_cb54": 0.0,
"ARB_long_0f74": 0.0
},
"last_alert": null
}

View File

@ -0,0 +1,189 @@
# CoinEx Live Futures Trader
**⚠️ REAL MONEY TRADING - EXTREME CAUTION REQUIRED ⚠️**
This is a live futures trading bot that connects to CoinEx and trades real money based on the paper trading strategy. Multiple safety mechanisms are implemented.
## 🛡️ Safety Features
### Kill Switch
- **Automatic Stop**: Bot automatically stops if account drops below 50% of starting balance
- **Position Closure**: Attempts to close all positions when kill switch triggers
- **Telegram Alert**: Sends immediate notification when triggered
- **Cannot be bypassed**: Checked before EVERY trading cycle
### Position Management
- **Max 3 positions** (down from 10 in paper trader)
- **Max 10x leverage** (no 15x trades allowed)
- **5% position sizing** (of current equity, ~$7.30 on $146 balance)
- **Futures only** - never touches spot trading
### Risk Controls
- **Stop Loss**: -3% on margin
- **Take Profit**: +5% on margin
- **Trailing Stop**: 2% from peak
- **Complete logging** of every trade
- **Error handling** - never crashes silently
## 📁 Files Created
- `coinex_live_trader.py` - Main trading bot
- `test_coinex_api.py` - API connection test
- `~/.config/systemd/user/coinex-live-trader.service` - Systemd service
- `~/.config/systemd/user/coinex-live-trader.timer` - 5-minute timer
- `COINEX_LIVE_TRADER.md` - This documentation
## 🚀 Getting Started
### 1. Test API Connection First
```bash
cd /home/wdjones/.openclaw/workspace/projects/crypto-signals/scripts
python3 test_coinex_api.py
```
This verifies:
- Credentials are loaded correctly
- API authentication works
- Balance, market data, and positions endpoints respond
### 2. Run Dry Run Mode
```bash
python3 coinex_live_trader.py --dry-run
```
Dry run mode does everything except place actual orders:
- ✅ Runs scanners
- ✅ Calculates position sizes
- ✅ Checks kill switch
- ✅ Logs all decisions
- ❌ No real orders placed
### 3. Live Trading (REAL MONEY)
```bash
python3 coinex_live_trader.py
```
**Only run this after thorough testing with dry-run mode!**
## 🔧 Systemd Service (DISABLED by default)
Service files are created but NOT enabled. To enable:
```bash
# Reload systemd configuration
systemctl --user daemon-reload
# Enable and start the timer
systemctl --user enable coinex-live-trader.timer
systemctl --user start coinex-live-trader.timer
# Check status
systemctl --user status coinex-live-trader.timer
```
To disable:
```bash
systemctl --user stop coinex-live-trader.timer
systemctl --user disable coinex-live-trader.timer
```
## 📊 Monitoring & Logs
### Log Files (in `/home/wdjones/.openclaw/workspace/projects/crypto-signals/data/coinex-live/`)
- `trades.log` - All trading activity
- `errors.log` - Error messages
- `trader_state.json` - Bot state (peak PnL tracking, starting balance)
### Systemd Logs
```bash
# View recent logs
journalctl --user -u coinex-live-trader.service -f
# View timer logs
journalctl --user -u coinex-live-trader.timer -f
```
### Telegram Alerts
- Position opens/closes
- Kill switch triggers
- Error notifications
- Periodic summaries
## ⚙️ Configuration
### Trading Parameters (in `coinex_live_trader.py`)
```python
POSITION_SIZE_PCT = 5.0 # 5% of equity per position
MAX_OPEN_POSITIONS = 3 # Max simultaneous positions
MAX_LEVERAGE = 10 # Leverage cap
SHORT_SCORE_THRESHOLD = 50 # Min score for shorts
LONG_SCORE_THRESHOLD = 45 # Min score for longs
TP_PCT = 5.0 # Take profit %
SL_PCT = -3.0 # Stop loss %
TRAILING_STOP_PCT = 2.0 # Trailing stop %
KILL_SWITCH_DRAWDOWN = 0.50 # 50% drawdown kill switch
```
### Scanning Logic
Uses the same short_scanner and spot scanner logic as the paper trader:
- **Short signals**: High RSI, above VWAP, overbought conditions
- **Long signals**: Low RSI, below VWAP, oversold conditions
- **Same coin list** as paper trader
## 🚨 Emergency Procedures
### Stop the Bot Immediately
```bash
# Stop the timer
systemctl --user stop coinex-live-trader.timer
# Kill any running instance
pkill -f coinex_live_trader.py
```
### Manual Position Closure
If you need to manually close positions, use the CoinEx web interface or API directly.
## 🔍 Key Differences from Paper Trader
| Feature | Paper Trader | Live Trader |
|---------|-------------|-------------|
| Position Size | Fixed $200 | 5% of equity (~$7.30) |
| Max Positions | 10 | 3 |
| Max Leverage | 15x | 10x |
| Kill Switch | None | 50% drawdown |
| Logging | Basic | Complete trade/error logs |
| Dry Run Mode | N/A | Available for testing |
## 📋 Pre-Launch Checklist
- [ ] API test passes (`test_coinex_api.py`)
- [ ] Dry run mode tested extensively
- [ ] Telegram alerts working
- [ ] Starting balance recorded in state file
- [ ] Log files created and writable
- [ ] Kill switch drawdown level confirmed (50%)
- [ ] Position sizing validated (5% = ~$7.30)
- [ ] Max positions confirmed (3)
- [ ] Leverage capped at 10x
## ⚠️ Important Warnings
1. **Real Money**: This trades with real money on real markets
2. **No Guarantees**: No strategy guarantees profits
3. **Monitor Closely**: Especially during first days of operation
4. **Market Risk**: Crypto futures are highly volatile
5. **API Risk**: Exchange issues could affect trading
6. **Kill Switch**: May not prevent all losses
7. **Test First**: Always test thoroughly in dry-run mode
## 📞 Support
- Check logs first: `trades.log` and `errors.log`
- Telegram alerts for real-time issues
- Systemd logs: `journalctl --user -u coinex-live-trader.service`
- Kill switch info in state file: `trader_state.json`
---
**Remember: This is real money trading. Start small, monitor closely, and never risk more than you can afford to lose.**

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Test script to verify CoinEx API connection and credentials.
Run this before using the live trader to ensure everything works.
"""
import sys
from pathlib import Path
# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from coinex_live_trader import CoinExAPI, load_coinex_credentials, setup_logging
def test_api():
"""Test CoinEx API connection and basic functionality."""
try:
print("=== CoinEx API Test ===")
# Setup logging directories
setup_logging()
print("Log directories created")
# Load credentials
print("Loading credentials...")
access_id, secret_key = load_coinex_credentials()
print(f"Access ID: {access_id[:8]}...")
# Initialize API
api = CoinExAPI(access_id, secret_key)
print("API client initialized")
# Test balance endpoint
print("\nTesting futures balance endpoint...")
balance = api.get_futures_balance()
print(f"Raw balance response: {balance}")
# Handle different response formats
if isinstance(balance, list):
if balance:
balance_data = balance[0] # Take first item if it's a list
# CoinEx uses different field names
total_balance = float(balance_data.get("available", 0)) + float(balance_data.get("frozen", 0))
available_balance = float(balance_data.get("available", 0))
else:
print("⚠️ Empty balance list returned")
total_balance = available_balance = 0
elif isinstance(balance, dict):
total_balance = float(balance.get("available", 0)) + float(balance.get("frozen", 0))
available_balance = float(balance.get("available", 0))
else:
print(f"⚠️ Unexpected balance format: {type(balance)}")
total_balance = available_balance = 0
print(f"✅ Balance retrieved successfully")
print(f" Total Balance: ${total_balance:.2f}")
print(f" Available Balance: ${available_balance:.2f}")
# Test market data endpoint
print("\nTesting market data endpoint...")
market_data = api.get_market_price("BTCUSDT")
if market_data:
print(f"✅ Market data retrieved successfully")
if isinstance(market_data, list):
print(f" Market data is a list with {len(market_data)} items")
if market_data:
print(f" First item keys: {list(market_data[0].keys()) if market_data[0] else 'Empty'}")
else:
print(f" Market data keys: {list(market_data.keys())}")
# Test positions endpoint
print("\nTesting positions endpoint...")
positions = api.get_positions()
if isinstance(positions, list):
print(f"✅ Positions retrieved successfully")
print(f" Number of open positions: {len(positions)}")
if positions:
for pos in positions[:3]: # Show first 3
market = pos.get("market", "Unknown")
side = pos.get("side", "Unknown")
amount = pos.get("amount", "0")
print(f" {market} {side} {amount}")
else:
print(f"✅ Positions endpoint working (empty/different format)")
print("\n🎉 All tests passed! API is working correctly.")
print("\nYou can now run the live trader with:")
print(" python3 coinex_live_trader.py --dry-run (for testing)")
print(" python3 coinex_live_trader.py (for live trading)")
return True
except Exception as e:
print(f"\n❌ Test failed: {e}")
# Just print error instead of using log_error which isn't available in this context
print(f"API test failed: {e}")
return False
if __name__ == "__main__":
success = test_api()
sys.exit(0 if success else 1)