Full sync - all projects, memory, configs
This commit is contained in:
11
projects/crypto-signals/.gitignore
vendored
Normal file
11
projects/crypto-signals/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
.next/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
*.db
|
||||
*.sqlite
|
||||
*.log
|
||||
venv/
|
||||
dist/
|
||||
.credentials/
|
||||
433
projects/crypto-signals/dashboard/index.html
Normal file
433
projects/crypto-signals/dashboard/index.html
Normal 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>
|
||||
18
projects/crypto-signals/dashboard/server.py
Normal file
18
projects/crypto-signals/dashboard/server.py
Normal 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
59
projects/crypto-signals/data/coinex-live/trader_config.json
Normal file
59
projects/crypto-signals/data/coinex-live/trader_config.json
Normal 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"
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"peak_pnl": {
|
||||
"TRUMPUSDT_short": 0.88,
|
||||
"PUMPUSDT_short": -2.7966
|
||||
},
|
||||
"last_alert": null,
|
||||
"starting_balance": 146.83821322
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
189
projects/crypto-signals/scripts/COINEX_LIVE_TRADER.md
Normal file
189
projects/crypto-signals/scripts/COINEX_LIVE_TRADER.md
Normal 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.**
|
||||
1123
projects/crypto-signals/scripts/coinex_live_trader.py
Normal file
1123
projects/crypto-signals/scripts/coinex_live_trader.py
Normal file
File diff suppressed because it is too large
Load Diff
101
projects/crypto-signals/scripts/test_coinex_api.py
Normal file
101
projects/crypto-signals/scripts/test_coinex_api.py
Normal 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)
|
||||
Reference in New Issue
Block a user