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>

376
src/api/server.py Normal file
View File

@ -0,0 +1,376 @@
"""
Simplified FastAPI server - working version
Removes the complex WebSocket manager that was causing issues
"""
import os
import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
import asyncpg
import csv
import io
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Database connection settings
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', 5432))
DB_NAME = os.getenv('DB_NAME', 'btc_data')
DB_USER = os.getenv('DB_USER', 'btc_bot')
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
async def get_db_pool():
"""Create database connection pool"""
return await asyncpg.create_pool(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
min_size=1,
max_size=10
)
pool = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage application lifespan"""
global pool
pool = await get_db_pool()
logger.info("API Server started successfully")
yield
if pool:
await pool.close()
logger.info("API Server stopped")
app = FastAPI(
title="BTC Bot Data API",
description="REST API for accessing BTC candle data",
version="1.1.0",
lifespan=lifespan
)
# Enable CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "BTC Bot Data API",
"docs": "/docs",
"dashboard": "/dashboard",
"status": "operational"
}
@app.get("/api/v1/candles")
async def get_candles(
symbol: str = Query("BTC", description="Trading pair symbol"),
interval: str = Query("1m", description="Candle interval"),
start: Optional[datetime] = Query(None, description="Start time (ISO format)"),
end: Optional[datetime] = Query(None, description="End time (ISO format)"),
limit: int = Query(1000, ge=1, le=10000, description="Maximum number of candles")
):
"""Get candle data for a symbol"""
async with pool.acquire() as conn:
query = """
SELECT time, symbol, interval, open, high, low, close, volume, validated
FROM candles
WHERE symbol = $1 AND interval = $2
"""
params = [symbol, interval]
if start:
query += f" AND time >= ${len(params) + 1}"
params.append(start)
if end:
query += f" AND time <= ${len(params) + 1}"
params.append(end)
query += f" ORDER BY time DESC LIMIT ${len(params) + 1}"
params.append(limit)
rows = await conn.fetch(query, *params)
return {
"symbol": symbol,
"interval": interval,
"count": len(rows),
"candles": [dict(row) for row in rows]
}
@app.get("/api/v1/candles/latest")
async def get_latest_candle(symbol: str = "BTC", interval: str = "1m"):
"""Get the most recent candle"""
async with pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT time, symbol, interval, open, high, low, close, volume
FROM candles
WHERE symbol = $1 AND interval = $2
ORDER BY time DESC
LIMIT 1
""", symbol, interval)
if not row:
raise HTTPException(status_code=404, detail="No data found")
return dict(row)
@app.get("/api/v1/stats")
async def get_stats(symbol: str = "BTC"):
"""Get trading statistics"""
async with pool.acquire() as conn:
# Get latest price and 24h stats
latest = await conn.fetchrow("""
SELECT close, time
FROM candles
WHERE symbol = $1 AND interval = '1m'
ORDER BY time DESC
LIMIT 1
""", symbol)
day_ago = await conn.fetchrow("""
SELECT close
FROM candles
WHERE symbol = $1 AND interval = '1m' AND time <= NOW() - INTERVAL '24 hours'
ORDER BY time DESC
LIMIT 1
""", symbol)
stats_24h = await conn.fetchrow("""
SELECT
MAX(high) as high_24h,
MIN(low) as low_24h,
SUM(volume) as volume_24h
FROM candles
WHERE symbol = $1 AND interval = '1m' AND time > NOW() - INTERVAL '24 hours'
""", symbol)
if not latest:
raise HTTPException(status_code=404, detail="No data found")
current_price = float(latest['close'])
previous_price = float(day_ago['close']) if day_ago else current_price
change_24h = ((current_price - previous_price) / previous_price * 100) if previous_price else 0
return {
"symbol": symbol,
"current_price": current_price,
"change_24h": round(change_24h, 2),
"high_24h": float(stats_24h['high_24h']) if stats_24h['high_24h'] else current_price,
"low_24h": float(stats_24h['low_24h']) if stats_24h['low_24h'] else current_price,
"volume_24h": float(stats_24h['volume_24h']) if stats_24h['volume_24h'] else 0,
"last_update": latest['time'].isoformat()
}
@app.get("/api/v1/health")
async def health_check():
"""System health check"""
try:
async with pool.acquire() as conn:
latest = await conn.fetchrow("""
SELECT symbol, MAX(time) as last_time, COUNT(*) as count
FROM candles
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY symbol
""")
return {
"status": "healthy",
"database": "connected",
"latest_candles": dict(latest) if latest else None,
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error(f"Health check failed: {e}")
raise HTTPException(status_code=503, detail=f"Health check failed: {str(e)}")
@app.get("/api/v1/ta")
async def get_technical_analysis(
symbol: str = Query("BTC", description="Trading pair symbol"),
interval: str = Query("1d", description="Candle interval")
):
"""
Get technical analysis for a symbol
Calculates MA 44, MA 125, trend, support/resistance
"""
try:
async with pool.acquire() as conn:
# Get enough candles for MA 125 calculation
rows = await conn.fetch("""
SELECT time, open, high, low, close, volume
FROM candles
WHERE symbol = $1 AND interval = $2
ORDER BY time DESC
LIMIT 200
""", symbol, interval)
if len(rows) < 50:
return {
"symbol": symbol,
"interval": interval,
"error": "Not enough data for technical analysis",
"min_required": 50,
"available": len(rows)
}
# Reverse to chronological order
candles = list(reversed(rows))
closes = [float(c['close']) for c in candles]
# Calculate Moving Averages
def calculate_ma(data, period):
if len(data) < period:
return None
return sum(data[-period:]) / period
ma_44 = calculate_ma(closes, 44)
ma_125 = calculate_ma(closes, 125)
current_price = closes[-1]
# Determine trend
if ma_44 and ma_125:
if current_price > ma_44 > ma_125:
trend = "Bullish"
trend_strength = "Strong" if current_price > ma_44 * 1.05 else "Moderate"
elif current_price < ma_44 < ma_125:
trend = "Bearish"
trend_strength = "Strong" if current_price < ma_44 * 0.95 else "Moderate"
else:
trend = "Neutral"
trend_strength = "Consolidation"
else:
trend = "Unknown"
trend_strength = "Insufficient data"
# Find support and resistance (recent swing points)
highs = [float(c['high']) for c in candles[-20:]]
lows = [float(c['low']) for c in candles[-20:]]
resistance = max(highs)
support = min(lows)
# Calculate price position
price_range = resistance - support
if price_range > 0:
position = (current_price - support) / price_range * 100
else:
position = 50
return {
"symbol": symbol,
"interval": interval,
"timestamp": datetime.utcnow().isoformat(),
"current_price": round(current_price, 2),
"moving_averages": {
"ma_44": round(ma_44, 2) if ma_44 else None,
"ma_125": round(ma_125, 2) if ma_125 else None,
"price_vs_ma44": round((current_price / ma_44 - 1) * 100, 2) if ma_44 else None,
"price_vs_ma125": round((current_price / ma_125 - 1) * 100, 2) if ma_125 else None
},
"trend": {
"direction": trend,
"strength": trend_strength,
"signal": "Buy" if trend == "Bullish" and trend_strength == "Strong" else
"Sell" if trend == "Bearish" and trend_strength == "Strong" else "Hold"
},
"levels": {
"resistance": round(resistance, 2),
"support": round(support, 2),
"position_in_range": round(position, 1)
},
"ai_placeholder": {
"available": False,
"message": "AI analysis available via Gemini or local LLM",
"action": "Click to analyze with AI"
}
}
except Exception as e:
logger.error(f"Technical analysis error: {e}")
raise HTTPException(status_code=500, detail=f"Technical analysis failed: {str(e)}")
@app.get("/api/v1/export/csv")
async def export_csv(
symbol: str = "BTC",
interval: str = "1m",
days: int = Query(7, ge=1, le=365, description="Number of days to export")
):
"""Export candle data to CSV"""
start_date = datetime.utcnow() - timedelta(days=days)
async with pool.acquire() as conn:
query = """
SELECT time, open, high, low, close, volume
FROM candles
WHERE symbol = $1 AND interval = $2 AND time >= $3
ORDER BY time
"""
rows = await conn.fetch(query, symbol, interval, start_date)
if not rows:
raise HTTPException(status_code=404, detail="No data found for export")
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['timestamp', 'open', 'high', 'low', 'close', 'volume'])
for row in rows:
writer.writerow([
row['time'].isoformat(),
row['open'],
row['high'],
row['low'],
row['close'],
row['volume']
])
output.seek(0)
return StreamingResponse(
io.BytesIO(output.getvalue().encode()),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename={symbol}_{interval}_{days}d.csv"
}
)
# Serve static files for dashboard
app.mount("/dashboard", StaticFiles(directory="src/api/dashboard/static", html=True), name="dashboard")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)