chart/static/app.js

117 lines
3.9 KiB
JavaScript

const { createApp, ref, computed, watch, onMounted, onUnmounted } = Vue;
const defaultPrefs = {
base_tf: '1m',
enabled: { ma: { '1d': [10, 20, 50, 100, 200], '4h': [], '1h': [] }, manual: true, auto: false },
hidden_levels_score: false,
};
createApp({
setup() {
const status = ref({ stream: 'disconnected', bars_held: {} });
const price = ref(null);
const storedPrefs = localStorage.getItem('chart-layer-prefs');
const prefs = ref(storedPrefs ? JSON.parse(storedPrefs) : structuredClone(defaultPrefs));
const timeframe = ref(prefs.value.base_tf || '1m');
const levels = ref([]);
const timeframes = ['1m', '2m', '5m', '15m', '30m', '1h', '4h', '1d'];
const now = ref(Date.now());
let chartApi = null;
let socket = null;
let timer = null;
const barAge = computed(() => {
if (!status.value.last_bar_t) return '—';
const seconds = Math.max(0, Math.floor(now.value / 1000 - status.value.last_bar_t));
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
});
async function refreshStatus() {
const response = await fetch('/api/status');
if (response.ok) status.value = await response.json();
}
function connect() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
socket = new WebSocket(`${protocol}://${location.host}/ws`);
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value }));
sendPrefs();
};
socket.onmessage = ({ data }) => {
const message = JSON.parse(data);
if (message.type === 'snapshot') {
chartApi.setBars(message.bars);
levels.value = message.levels || [];
syncVisibleLevels();
price.value = message.price;
} else if (message.type === 'bar') {
chartApi.updateBar(message.bar);
price.value = message.bar.c;
status.value.last_bar_t = message.bar.t;
} else if (message.type === 'levels') {
levels.value = message.levels;
syncVisibleLevels();
}
};
socket.onclose = () => {
status.value.stream = 'disconnected';
setTimeout(connect, 2000);
};
}
function selectTimeframe(tf) {
timeframe.value = tf;
prefs.value.base_tf = tf;
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'subscribe', tf }));
}
}
function enabled(level) {
if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period);
if (level.kind === 'manual') return prefs.value.enabled.manual;
return prefs.value.enabled.auto;
}
function syncVisibleLevels() {
if (chartApi) chartApi.syncLevels(levels.value.filter(enabled));
}
function sendPrefs() {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'prefs', ...prefs.value }));
}
}
function allEnabled(tf) {
const available = tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21];
return available.every(period => (prefs.value.enabled.ma[tf] || []).includes(period));
}
function toggleGroup(tf, checked) {
prefs.value.enabled.ma[tf] = checked ? (tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21]) : [];
}
watch(prefs, () => {
localStorage.setItem('chart-layer-prefs', JSON.stringify(prefs.value));
syncVisibleLevels();
sendPrefs();
}, { deep: true });
onMounted(() => {
chartApi = new ConfluenceChart();
chartApi.create(document.getElementById('chart'));
refreshStatus();
connect();
timer = setInterval(() => { now.value = Date.now(); refreshStatus(); }, 5000);
});
onUnmounted(() => {
clearInterval(timer);
if (socket) socket.close();
if (chartApi) chartApi.destroy();
});
return { status, price, barAge, timeframe, timeframes, prefs, selectTimeframe, allEnabled, toggleGroup };
},
}).mount('#app');