Initial commit: BTC Bot with dashboard, TA analysis, and 14 timeframes

This commit is contained in:
BTC Bot
2026-02-11 22:27:51 +01:00
commit 933537d759
32 changed files with 4689 additions and 0 deletions

View File

@ -0,0 +1,842 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Trading Dashboard</title>
<script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script>
<style>
:root {
--tv-bg: #131722;
--tv-panel-bg: #1e222d;
--tv-border: #2a2e39;
--tv-text: #d1d4dc;
--tv-text-secondary: #787b86;
--tv-green: #26a69a;
--tv-red: #ef5350;
--tv-blue: #2962ff;
--tv-hover: #2a2e39;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--tv-bg);
color: var(--tv-text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.toolbar {
background: var(--tv-panel-bg);
border-bottom: 1px solid var(--tv-border);
padding: 8px 16px;
display: flex;
align-items: center;
gap: 16px;
height: 56px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
overflow: hidden;
}
.symbol-badge {
background: var(--tv-bg);
padding: 6px 12px;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
border: 1px solid var(--tv-border);
white-space: nowrap;
}
.timeframe-scroll {
display: flex;
gap: 2px;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--tv-border) transparent;
flex: 1;
}
.timeframe-scroll::-webkit-scrollbar {
height: 4px;
}
.timeframe-scroll::-webkit-scrollbar-track {
background: transparent;
}
.timeframe-scroll::-webkit-scrollbar-thumb {
background: var(--tv-border);
border-radius: 2px;
}
.timeframe-btn {
background: transparent;
border: none;
color: var(--tv-text-secondary);
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
min-width: 36px;
}
.timeframe-btn:hover {
background: var(--tv-hover);
color: var(--tv-text);
}
.timeframe-btn.active {
background: var(--tv-blue);
color: white;
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
white-space: nowrap;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--tv-green);
}
.status-text {
font-size: 12px;
color: var(--tv-text-secondary);
}
.stats-panel {
background: var(--tv-panel-bg);
border-bottom: 1px solid var(--tv-border);
padding: 8px 16px;
display: flex;
gap: 32px;
height: 44px;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 10px;
color: var(--tv-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
}
.stat-value.positive { color: var(--tv-green); }
.stat-value.negative { color: var(--tv-red); }
.main-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chart-wrapper {
flex: 2;
position: relative;
background: var(--tv-bg);
min-height: 0;
}
#chart {
width: 100%;
height: 100%;
}
.ta-panel {
flex: 1;
background: var(--tv-panel-bg);
border-top: 1px solid var(--tv-border);
display: flex;
flex-direction: column;
min-height: 200px;
max-height: 400px;
}
.ta-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--tv-border);
background: var(--tv-bg);
}
.ta-title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.ta-interval {
background: var(--tv-blue);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
.ta-actions {
display: flex;
gap: 8px;
}
.ta-btn {
background: var(--tv-bg);
border: 1px solid var(--tv-border);
color: var(--tv-text);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.ta-btn:hover {
background: var(--tv-hover);
border-color: var(--tv-blue);
}
.ta-btn.ai-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
}
.ta-btn.ai-btn:hover {
opacity: 0.9;
}
.ta-last-update {
font-size: 11px;
color: var(--tv-text-secondary);
}
.ta-content {
flex: 1;
padding: 16px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
overflow-y: auto;
}
.ta-section {
background: var(--tv-bg);
border: 1px solid var(--tv-border);
border-radius: 8px;
padding: 12px;
}
.ta-section-title {
font-size: 11px;
color: var(--tv-text-secondary);
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.ta-trend {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
}
.ta-trend.bullish { color: var(--tv-green); }
.ta-trend.bearish { color: var(--tv-red); }
.ta-trend.neutral { color: var(--tv-text-secondary); }
.ta-strength {
font-size: 12px;
color: var(--tv-text-secondary);
margin-top: 4px;
}
.ta-signal {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-top: 8px;
}
.ta-signal.buy {
background: rgba(38, 166, 154, 0.2);
color: var(--tv-green);
}
.ta-signal.sell {
background: rgba(239, 83, 80, 0.2);
color: var(--tv-red);
}
.ta-signal.hold {
background: rgba(120, 123, 134, 0.2);
color: var(--tv-text-secondary);
}
.ta-ma-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--tv-border);
font-size: 13px;
}
.ta-ma-row:last-child {
border-bottom: none;
}
.ta-ma-label {
color: var(--tv-text-secondary);
}
.ta-ma-value {
font-weight: 600;
}
.ta-ma-change {
font-size: 11px;
margin-left: 4px;
}
.ta-ma-change.positive { color: var(--tv-green); }
.ta-ma-change.negative { color: var(--tv-red); }
.ta-level {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
}
.ta-level-label {
color: var(--tv-text-secondary);
}
.ta-level-value {
font-weight: 600;
font-family: 'Courier New', monospace;
}
.ta-position-bar {
width: 100%;
height: 6px;
background: var(--tv-border);
border-radius: 3px;
margin-top: 8px;
position: relative;
}
.ta-position-marker {
position: absolute;
width: 12px;
height: 12px;
background: var(--tv-blue);
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--tv-bg);
}
.ta-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--tv-text-secondary);
font-size: 14px;
}
.ta-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--tv-red);
font-size: 14px;
}
@media (max-width: 1200px) {
.ta-content {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.ta-content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="toolbar">
<div class="toolbar-left">
<span class="symbol-badge">BTC/USD</span>
<div class="timeframe-scroll" id="timeframeContainer">
<!-- Timeframes will be inserted here by JS -->
</div>
</div>
<div class="connection-status">
<div class="status-dot" id="statusDot"></div>
<span class="status-text" id="statusText">Live</span>
</div>
</div>
<div class="stats-panel">
<div class="stat-item">
<span class="stat-label">Price</span>
<span class="stat-value" id="currentPrice">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Change</span>
<span class="stat-value" id="priceChange">--</span>
</div>
<div class="stat-item">
<span class="stat-label">High</span>
<span class="stat-value" id="dailyHigh">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Low</span>
<span class="stat-value" id="dailyLow">--</span>
</div>
</div>
<div class="main-container">
<div class="chart-wrapper">
<div id="chart"></div>
</div>
<div class="ta-panel" id="taPanel">
<div class="ta-header">
<div class="ta-title">
Technical Analysis
<span class="ta-interval" id="taInterval">1D</span>
</div>
<div class="ta-actions">
<span class="ta-last-update" id="taLastUpdate">--</span>
<button class="ta-btn ai-btn" id="aiBtn" onclick="openAIAnalysis()">
🤖 AI Analysis
</button>
<button class="ta-btn" onclick="refreshTA()">
🔄 Refresh
</button>
</div>
</div>
<div class="ta-content" id="taContent">
<div class="ta-loading">Loading technical analysis...</div>
</div>
</div>
</div>
<script>
class TradingDashboard {
constructor() {
this.chart = null;
this.candleSeries = null;
this.currentInterval = '1d';
this.intervals = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
this.allData = new Map();
this.isLoading = false;
this.hasInitialLoad = false;
this.taData = null;
this.init();
}
init() {
this.createTimeframeButtons();
this.initChart();
this.initEventListeners();
this.loadInitialData();
this.loadTA();
setInterval(() => this.loadNewData(), 15000);
}
createTimeframeButtons() {
const container = document.getElementById('timeframeContainer');
this.intervals.forEach(interval => {
const btn = document.createElement('button');
btn.className = 'timeframe-btn';
btn.dataset.interval = interval;
btn.textContent = interval;
if (interval === this.currentInterval) {
btn.classList.add('active');
}
btn.addEventListener('click', () => this.switchTimeframe(interval));
container.appendChild(btn);
});
}
initChart() {
const chartContainer = document.getElementById('chart');
this.chart = LightweightCharts.createChart(chartContainer, {
layout: {
background: { color: '#131722' },
textColor: '#d1d4dc',
},
grid: {
vertLines: { color: '#2a2e39' },
horzLines: { color: '#2a2e39' },
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: '#2a2e39',
},
timeScale: {
borderColor: '#2a2e39',
timeVisible: true,
},
});
this.candleSeries = this.chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
});
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
window.addEventListener('resize', () => {
this.chart.applyOptions({
width: chartContainer.clientWidth,
height: chartContainer.clientHeight,
});
});
}
initEventListeners() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
const shortcuts = {
'1': '1m', '2': '3m', '3': '5m', '4': '15m', '5': '30m',
'6': '1h', '7': '2h', '8': '4h', '9': '8h', '0': '12h',
'd': '1d', 'D': '1d', 'w': '1w', 'W': '1w', 'm': '1M', 'M': '1M'
};
if (shortcuts[e.key]) {
this.switchTimeframe(shortcuts[e.key]);
}
});
}
async loadInitialData() {
await this.loadData(500, true);
this.hasInitialLoad = true;
}
async loadData(limit = 500, fitToContent = false) {
if (this.isLoading) return;
this.isLoading = true;
try {
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
if (fitToContent) {
this.chart.timeScale().fitContent();
} else if (visibleRange) {
this.chart.timeScale().setVisibleLogicalRange(visibleRange);
}
const latest = mergedData[mergedData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
}
async loadNewData() {
if (!this.hasInitialLoad) return;
try {
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=100`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
const latest = mergedData[mergedData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading new data:', error);
}
}
mergeData(existing, newData) {
const dataMap = new Map();
existing.forEach(c => dataMap.set(c.time, c));
newData.forEach(c => dataMap.set(c.time, c));
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
}
onVisibleRangeChange() {
if (!this.hasInitialLoad || this.isLoading) return;
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
if (!visibleRange) return;
const data = this.candleSeries.data();
if (!data || data.length === 0) return;
if (visibleRange.from < 10) {
const oldestCandle = data[0];
if (oldestCandle) {
this.loadHistoricalData(oldestCandle.time);
}
}
}
async loadHistoricalData(beforeTime) {
if (this.isLoading) return;
this.isLoading = true;
try {
const endTime = new Date(beforeTime * 1000);
const startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000);
const response = await fetch(
`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&start=${startTime.toISOString()}&end=${endTime.toISOString()}&limit=500`
);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
}));
const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
}
} catch (error) {
console.error('Error loading historical data:', error);
} finally {
this.isLoading = false;
}
}
async loadTA() {
try {
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
this.taData = await response.json();
this.renderTA();
} catch (error) {
console.error('Error loading TA:', error);
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis</div>';
}
}
renderTA() {
if (!this.taData || this.taData.error) {
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
return;
}
const data = this.taData;
const trendClass = data.trend.direction.toLowerCase();
const signalClass = data.trend.signal.toLowerCase();
const ma44Change = data.moving_averages.price_vs_ma44;
const ma125Change = data.moving_averages.price_vs_ma125;
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
document.getElementById('taContent').innerHTML = `
<div class="ta-section">
<div class="ta-section-title">Trend Analysis</div>
<div class="ta-trend ${trendClass}">
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
</div>
<div class="ta-strength">${data.trend.strength}</div>
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
</div>
<div class="ta-section">
<div class="ta-section-title">Moving Averages</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 44</span>
<span class="ta-ma-value">
${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'}
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
</span>
</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 125</span>
<span class="ta-ma-value">
${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'}
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
</span>
</div>
</div>
<div class="ta-section">
<div class="ta-section-title">Key Levels</div>
<div class="ta-level">
<span class="ta-level-label">Resistance</span>
<span class="ta-level-value">$${data.levels.resistance.toLocaleString()}</span>
</div>
<div class="ta-level">
<span class="ta-level-label">Support</span>
<span class="ta-level-value">$${data.levels.support.toLocaleString()}</span>
</div>
<div class="ta-position-bar">
<div class="ta-position-marker" style="left: ${data.levels.position_in_range}%"></div>
</div>
<div style="font-size: 11px; color: var(--tv-text-secondary); margin-top: 4px; text-align: center;">
Position in range: ${data.levels.position_in_range.toFixed(1)}%
</div>
</div>
<div class="ta-section">
<div class="ta-section-title">Price Info</div>
<div class="ta-level">
<span class="ta-level-label">Current</span>
<span class="ta-level-value">$${data.current_price.toLocaleString()}</span>
</div>
<div style="font-size: 12px; color: var(--tv-text-secondary); margin-top: 8px;">
Based on last 200 candles<br>
Strategy: Trend following with MA crossovers
</div>
</div>
`;
}
updateStats(candle) {
const price = candle.close;
const change = ((price - candle.open) / candle.open * 100);
document.getElementById('currentPrice').textContent = price.toFixed(2);
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('dailyHigh').textContent = candle.high.toFixed(2);
document.getElementById('dailyLow').textContent = candle.low.toFixed(2);
}
switchTimeframe(interval) {
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
this.currentInterval = interval;
this.hasInitialLoad = false;
document.querySelectorAll('.timeframe-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.interval === interval);
});
this.allData.delete(interval);
this.loadInitialData();
this.loadTA();
}
}
function refreshTA() {
if (window.dashboard) {
window.dashboard.loadTA();
}
}
function openAIAnalysis() {
const symbol = 'BTC';
const interval = window.dashboard?.currentInterval || '1d';
const prompt = `Analyze Bitcoin (${symbol}) ${interval} chart. Current trend, support/resistance levels, and trading recommendation. Technical indicators: MA44, MA125.`;
const geminiUrl = `https://gemini.google.com/app?prompt=${encodeURIComponent(prompt)}`;
window.open(geminiUrl, '_blank');
}
document.addEventListener('DOMContentLoaded', () => {
window.dashboard = new TradingDashboard();
});
</script>
</body>
</html>