Bitunix Charts - OpenCode Skill
Project Context
Building a TradingView-style desktop charting app for Ubuntu 24.04 with:
- •Stack: Electron + React + TypeScript + Vite
- •Charting: lightweight-charts (TradingView's library)
- •State: Zustand
- •Styling: Tailwind CSS + shadcn/ui
- •Data: Bitunix exchange WebSocket + REST API
- •Indicators: 25 technical indicators (max 10 active)
- •Output: .deb package with desktop icon
Critical Patterns
Electron Structure
code
src/
main/ # Node.js process - API calls, WebSocket, IPC
main.ts
preload.ts # contextBridge for renderer
ipc-handlers.ts
renderer/ # React app - UI only, no direct Node access
App.tsx
components/
stores/
services/
hooks/
IPC Communication
typescript
// preload.ts - expose safe APIs
contextBridge.exposeInMainWorld('bitunix', {
getKlines: (symbol, interval) => ipcRenderer.invoke('bitunix:get-klines', symbol, interval),
subscribe: (channel, callback) => {
const subscription = (_event, data) => callback(data);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
}
});
// renderer usage
const klines = await window.bitunix.getKlines('BTCUSDT', '1h');
Lightweight Charts Setup
typescript
import { createChart, ColorType } from 'lightweight-charts';
const chart = createChart(container, {
layout: {
background: { type: ColorType.Solid, color: '#131722' },
textColor: '#d1d4dc',
},
grid: {
vertLines: { color: '#1e222d' },
horzLines: { color: '#1e222d' },
},
crosshair: { mode: 1 },
rightPriceScale: { borderColor: '#2a2e39' },
timeScale: { borderColor: '#2a2e39', timeVisible: true },
});
const candleSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
});
Zustand Store Pattern
typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface IndicatorStore {
activeIndicators: IndicatorConfig[];
addIndicator: (type: IndicatorType) => void;
// ...
}
export const useIndicatorStore = create<IndicatorStore>()(
persist(
(set, get) => ({
activeIndicators: [],
addIndicator: (type) => {
const current = get().activeIndicators;
if (current.length >= 10) return; // MAX 10
// ...
},
}),
{ name: 'indicator-storage' }
)
);
Bitunix API Reference
REST Endpoints
code
Base URL: https://api.bitunix.com GET /api/v1/market/symbols # All trading pairs GET /api/v1/market/klines # Candlestick data ?symbol=BTCUSDT &interval=1h # 1m,5m,15m,30m,1h,4h,1d,1w &limit=500 # max 1000 GET /api/v1/market/ticker/24hr # 24hr stats ?symbol=BTCUSDT
WebSocket
code
URL: wss://ws.bitunix.com/ws
// Subscribe to klines
{
"method": "SUBSCRIBE",
"params": ["btcusdt@kline_1h"],
"id": 1
}
// Kline update message
{
"e": "kline",
"s": "BTCUSDT",
"k": {
"t": 1699900800000, // open time
"o": "42150.00", // open
"h": "42200.00", // high
"l": "42100.00", // low
"c": "42175.50", // close
"v": "1234.56", // volume
"x": false // is closed
}
}
Indicator Implementation
Using technicalindicators Package
typescript
import { SMA, EMA, RSI, MACD, BollingerBands } from 'technicalindicators';
// SMA
const smaValues = SMA.calculate({ period: 20, values: closes });
// MACD
const macdValues = MACD.calculate({
values: closes,
fastPeriod: 12,
slowPeriod: 26,
signalPeriod: 9,
SimpleMAOscillator: false,
SimpleMASignal: false,
});
// Bollinger
const bbValues = BollingerBands.calculate({
period: 20,
values: closes,
stdDev: 2,
});
Custom Implementations Needed
typescript
// VWAP - reset daily
function calculateVWAP(klines: Kline[]): number[] {
let cumTypicalPriceVol = 0;
let cumVolume = 0;
let currentDay = -1;
return klines.map(k => {
const day = new Date(k.openTime).getDate();
if (day !== currentDay) {
cumTypicalPriceVol = 0;
cumVolume = 0;
currentDay = day;
}
const typicalPrice = (k.high + k.low + k.close) / 3;
cumTypicalPriceVol += typicalPrice * k.volume;
cumVolume += k.volume;
return cumTypicalPriceVol / cumVolume;
});
}
// Supertrend
function calculateSupertrend(klines: Kline[], period = 10, multiplier = 3) {
const atr = ATR.calculate({ high: highs, low: lows, close: closes, period });
// ... implementation
}
File Templates
Component Template
typescript
import { FC, useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
interface Props {
className?: string;
}
export const ComponentName: FC<Props> = ({ className }) => {
return (
<div className={cn('', className)}>
{/* content */}
</div>
);
};
Store Template
typescript
import { create } from 'zustand';
interface StoreState {
// state
}
interface StoreActions {
// actions
}
export const useStoreName = create<StoreState & StoreActions>((set, get) => ({
// implementation
}));
Color Palette
css
--bg-primary: #131722; --bg-secondary: #1e222d; --bg-tertiary: #2a2e39; --text-primary: #d1d4dc; --text-secondary: #787b86; --accent: #2962ff; --success: #26a69a; --danger: #ef5350; --warning: #f7931a;
Common Issues & Fixes
Electron + Vite HMR
typescript
// vite.config.ts
export default defineConfig({
plugins: [react()],
base: './',
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: 5173,
strictPort: true,
},
});
Chart Resize
typescript
useEffect(() => {
const handleResize = () => {
chart.applyOptions({ width: container.clientWidth });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
WebSocket Reconnect
typescript
const reconnect = () => {
setTimeout(() => {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
setTimeout(connect, delay);
}, 0);
};
Build Commands
bash
# Development npm run electron:dev # Build for Linux npm run electron:build:linux # Build .deb only npm run electron:build:deb # The .deb will be in dist/