- Set ruff/black line length to 120 - Reformatted code with black - Fixed import ordering with ruff - Disabled lint for UI component files with long CSS strings - Updated pyproject.toml with proper tool configuration
174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from nicegui import ui
|
|
|
|
_CHARTS_SCRIPT_ADDED = False
|
|
|
|
|
|
def _ensure_lightweight_charts_assets() -> None:
|
|
global _CHARTS_SCRIPT_ADDED
|
|
if _CHARTS_SCRIPT_ADDED:
|
|
return
|
|
ui.add_head_html("""
|
|
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
|
""")
|
|
_CHARTS_SCRIPT_ADDED = True
|
|
|
|
|
|
class CandlestickChart:
|
|
"""Minimal Lightweight-Charts wrapper for NiceGUI candlestick dashboards.
|
|
|
|
Features:
|
|
- real-time candlestick price updates
|
|
- volume histogram overlay
|
|
- moving-average / indicator line support
|
|
"""
|
|
|
|
def __init__(self, title: str = "Gold Price", *, height: int = 420) -> None:
|
|
_ensure_lightweight_charts_assets()
|
|
self.chart_id = f"chart_{uuid4().hex}"
|
|
self.height = height
|
|
|
|
with ui.card().classes("w-full rounded-2xl border border-slate-800 bg-slate-950/90 shadow-xl"):
|
|
with ui.row().classes("w-full items-center justify-between"):
|
|
ui.label(title).classes("text-lg font-semibold text-white")
|
|
ui.label("Live").classes(
|
|
"rounded-full bg-emerald-500/15 px-3 py-1 text-xs font-medium uppercase tracking-wide text-emerald-300"
|
|
)
|
|
self.container = ui.html(f'<div id="{self.chart_id}" class="w-full rounded-xl"></div>').style(
|
|
f"height: {height}px;"
|
|
)
|
|
|
|
self._initialize_chart()
|
|
|
|
def _initialize_chart(self) -> None:
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const root = document.getElementById({json.dumps(self.chart_id)});
|
|
if (!root || typeof LightweightCharts === 'undefined') return;
|
|
|
|
root.innerHTML = '';
|
|
window.vaultDashCharts = window.vaultDashCharts || {{}};
|
|
|
|
const chart = LightweightCharts.createChart(root, {{
|
|
autoSize: true,
|
|
layout: {{
|
|
background: {{ color: '#020617' }},
|
|
textColor: '#cbd5e1',
|
|
}},
|
|
grid: {{
|
|
vertLines: {{ color: 'rgba(148, 163, 184, 0.12)' }},
|
|
horzLines: {{ color: 'rgba(148, 163, 184, 0.12)' }},
|
|
}},
|
|
rightPriceScale: {{ borderColor: 'rgba(148, 163, 184, 0.25)' }},
|
|
timeScale: {{ borderColor: 'rgba(148, 163, 184, 0.25)' }},
|
|
crosshair: {{ mode: LightweightCharts.CrosshairMode.Normal }},
|
|
}});
|
|
|
|
const candleSeries = chart.addSeries(LightweightCharts.CandlestickSeries, {{
|
|
upColor: '#22c55e',
|
|
downColor: '#ef4444',
|
|
borderVisible: false,
|
|
wickUpColor: '#22c55e',
|
|
wickDownColor: '#ef4444',
|
|
}});
|
|
|
|
const volumeSeries = chart.addSeries(LightweightCharts.HistogramSeries, {{
|
|
priceFormat: {{ type: 'volume' }},
|
|
priceScaleId: '',
|
|
scaleMargins: {{ top: 0.78, bottom: 0 }},
|
|
color: 'rgba(56, 189, 248, 0.45)',
|
|
}});
|
|
|
|
window.vaultDashCharts[{json.dumps(self.chart_id)}] = {{
|
|
chart,
|
|
candleSeries,
|
|
volumeSeries,
|
|
indicators: {{}},
|
|
}};
|
|
}})();
|
|
""")
|
|
|
|
def set_candles(self, candles: list[dict[str, Any]]) -> None:
|
|
payload = json.dumps(candles)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
if (!ref) return;
|
|
ref.candleSeries.setData({payload});
|
|
ref.chart.timeScale().fitContent();
|
|
}})();
|
|
""")
|
|
|
|
def update_price(self, candle: dict[str, Any]) -> None:
|
|
payload = json.dumps(candle)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
if (!ref) return;
|
|
ref.candleSeries.update({payload});
|
|
}})();
|
|
""")
|
|
|
|
def set_volume(self, volume_points: list[dict[str, Any]]) -> None:
|
|
payload = json.dumps(volume_points)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
if (!ref) return;
|
|
ref.volumeSeries.setData({payload});
|
|
}})();
|
|
""")
|
|
|
|
def update_volume(self, volume_point: dict[str, Any]) -> None:
|
|
payload = json.dumps(volume_point)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
if (!ref) return;
|
|
ref.volumeSeries.update({payload});
|
|
}})();
|
|
""")
|
|
|
|
def set_indicator(
|
|
self,
|
|
name: str,
|
|
points: list[dict[str, Any]],
|
|
*,
|
|
color: str = "#f59e0b",
|
|
line_width: int = 2,
|
|
) -> None:
|
|
key = json.dumps(name)
|
|
payload = json.dumps(points)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
if (!ref) return;
|
|
if (!ref.indicators[{key}]) {{
|
|
ref.indicators[{key}] = ref.chart.addSeries(LightweightCharts.LineSeries, {{
|
|
color: {json.dumps(color)},
|
|
lineWidth: {line_width},
|
|
priceLineVisible: false,
|
|
lastValueVisible: true,
|
|
}});
|
|
}}
|
|
ref.indicators[{key}].setData({payload});
|
|
}})();
|
|
""")
|
|
|
|
def update_indicator(self, name: str, point: dict[str, Any]) -> None:
|
|
key = json.dumps(name)
|
|
payload = json.dumps(point)
|
|
ui.run_javascript(f"""
|
|
(function() {{
|
|
const ref = window.vaultDashCharts?.[{json.dumps(self.chart_id)}];
|
|
const series = ref?.indicators?.[{key}];
|
|
if (!series) return;
|
|
series.update({payload});
|
|
}})();
|
|
""")
|