Improve dashboard rendering stability and increase DB pool size

This commit is contained in:
BTC Bot
2026-02-12 08:39:54 +01:00
parent f9559d1116
commit 38f0a21f56
3 changed files with 84 additions and 21 deletions

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Trading Dashboard</title> <title>BTC Trading Dashboard</title>
<link rel="icon" href="data:,">
<script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script>
<style> <style>
:root { :root {
@ -492,11 +493,29 @@
this.loadInitialData(); this.loadInitialData();
this.loadTA(); this.loadTA();
setInterval(() => this.loadNewData(), 15000); // Refresh every 10 seconds for new data
setInterval(() => {
this.loadNewData();
// Occasionally refresh TA
if (new Date().getSeconds() < 15) this.loadTA();
}, 10000);
}
isAtRightEdge() {
const timeScale = this.chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return true;
const data = this.candleSeries.data();
if (!data || data.length === 0) return true;
// If the right-most visible bar is within 5 bars of the last data point
return visibleRange.to >= data.length - 5;
} }
createTimeframeButtons() { createTimeframeButtons() {
const container = document.getElementById('timeframeContainer'); const container = document.getElementById('timeframeContainer');
container.innerHTML = ''; // Clear existing
this.intervals.forEach(interval => { this.intervals.forEach(interval => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'timeframe-btn'; btn.className = 'timeframe-btn';
@ -527,10 +546,17 @@
}, },
rightPriceScale: { rightPriceScale: {
borderColor: '#2a2e39', borderColor: '#2a2e39',
autoScale: true,
}, },
timeScale: { timeScale: {
borderColor: '#2a2e39', borderColor: '#2a2e39',
timeVisible: true, timeVisible: true,
secondsVisible: false,
rightOffset: 12,
barSpacing: 10,
},
handleScroll: {
vertTouchDrag: false,
}, },
}); });
@ -551,6 +577,18 @@
height: chartContainer.clientHeight, height: chartContainer.clientHeight,
}); });
}); });
// Handle tab visibility and focus
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.loadNewData();
this.loadTA();
}
});
window.addEventListener('focus', () => {
this.loadNewData();
this.loadTA();
});
} }
initEventListeners() { initEventListeners() {
@ -570,11 +608,11 @@
} }
async loadInitialData() { async loadInitialData() {
await this.loadData(500, true); await this.loadData(1000, true);
this.hasInitialLoad = true; this.hasInitialLoad = true;
} }
async loadData(limit = 500, fitToContent = false) { async loadData(limit = 1000, fitToContent = false) {
if (this.isLoading) return; if (this.isLoading) return;
this.isLoading = true; this.isLoading = true;
@ -616,13 +654,21 @@
} }
async loadNewData() { async loadNewData() {
if (!this.hasInitialLoad) return; if (!this.hasInitialLoad || this.isLoading) return;
try { try {
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=100`); const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
const data = await response.json(); const data = await response.json();
if (data.candles && data.candles.length > 0) { if (data.candles && data.candles.length > 0) {
const atEdge = this.isAtRightEdge();
// Get current data from series to find the last timestamp
const currentSeriesData = this.candleSeries.data();
const lastTimestamp = currentSeriesData.length > 0
? currentSeriesData[currentSeriesData.length - 1].time
: 0;
const chartData = data.candles.reverse().map(c => ({ const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000), time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open), open: parseFloat(c.open),
@ -631,13 +677,24 @@
close: parseFloat(c.close) close: parseFloat(c.close)
})); }));
// Only update with data that is newer than or equal to the last candle
// update() handles equal timestamps by updating the existing bar
chartData.forEach(candle => {
if (candle.time >= lastTimestamp) {
this.candleSeries.update(candle);
}
});
// Update cache
const existingData = this.allData.get(this.currentInterval) || []; const existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData); this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData); // If we were at the edge, scroll to show the new candle
if (atEdge) {
this.chart.timeScale().scrollToRealTime();
}
const latest = mergedData[mergedData.length - 1]; const latest = chartData[chartData.length - 1];
this.updateStats(latest); this.updateStats(latest);
} }
} catch (error) { } catch (error) {

View File

@ -39,8 +39,9 @@ async def get_db_pool():
database=DB_NAME, database=DB_NAME,
user=DB_USER, user=DB_USER,
password=DB_PASSWORD, password=DB_PASSWORD,
min_size=1, min_size=2,
max_size=10 max_size=20,
max_inactive_connection_lifetime=300
) )

View File

@ -28,14 +28,14 @@ class DatabaseManager:
database: str = None, database: str = None,
user: str = None, user: str = None,
password: str = None, password: str = None,
pool_size: int = 5 pool_size: int = 20
): ):
self.host = host or os.getenv('DB_HOST', 'localhost') self.host = host or os.getenv('DB_HOST', 'localhost')
self.port = port or int(os.getenv('DB_PORT', 5432)) self.port = port or int(os.getenv('DB_PORT', 5432))
self.database = database or os.getenv('DB_NAME', 'btc_data') self.database = database or os.getenv('DB_NAME', 'btc_data')
self.user = user or os.getenv('DB_USER', 'btc_bot') self.user = user or os.getenv('DB_USER', 'btc_bot')
self.password = password or os.getenv('DB_PASSWORD', '') self.password = password or os.getenv('DB_PASSWORD', '')
self.pool_size = pool_size self.pool_size = int(os.getenv('DB_POOL_SIZE', pool_size))
self.pool: Optional[Pool] = None self.pool: Optional[Pool] = None
@ -48,17 +48,18 @@ class DatabaseManager:
database=self.database, database=self.database,
user=self.user, user=self.user,
password=self.password, password=self.password,
min_size=1, min_size=2,
max_size=self.pool_size, max_size=self.pool_size,
command_timeout=60 command_timeout=60,
max_inactive_connection_lifetime=300
) )
# Test connection # Test connection
async with self.pool.acquire() as conn: async with self.acquire() as conn:
version = await conn.fetchval('SELECT version()') version = await conn.fetchval('SELECT version()')
logger.info(f"Connected to database: {version[:50]}...") logger.info(f"Connected to database: {version[:50]}...")
logger.info(f"Database pool created (size: {self.pool_size})") logger.info(f"Database pool created (min: 2, max: {self.pool_size})")
except Exception as e: except Exception as e:
logger.error(f"Failed to connect to database: {type(e).__name__}: {e!r}") logger.error(f"Failed to connect to database: {type(e).__name__}: {e!r}")
@ -71,12 +72,16 @@ class DatabaseManager:
logger.info("Database pool closed") logger.info("Database pool closed")
@asynccontextmanager @asynccontextmanager
async def acquire(self): async def acquire(self, timeout: float = 30.0):
"""Context manager for acquiring connection""" """Context manager for acquiring connection with timeout"""
if not self.pool: if not self.pool:
raise RuntimeError("Database not connected") raise RuntimeError("Database not connected")
async with self.pool.acquire() as conn: try:
yield conn async with self.pool.acquire(timeout=timeout) as conn:
yield conn
except asyncio.TimeoutError:
logger.error(f"Database connection acquisition timed out after {timeout}s")
raise
async def insert_candles(self, candles: List[Candle]) -> int: async def insert_candles(self, candles: List[Candle]) -> int:
""" """