Implement M3.5 persistent layer controls
This commit is contained in:
parent
7f2fcc2020
commit
ff7d9c6e1c
4 changed files with 71 additions and 7 deletions
|
|
@ -27,15 +27,18 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||||
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
|
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
|
||||||
runtime.subscribers.add(queue)
|
runtime.subscribers.add(queue)
|
||||||
tf = Timeframe.M1
|
tf = Timeframe.M1
|
||||||
|
prefs = None
|
||||||
await websocket.send_json(snapshot(runtime, tf))
|
await websocket.send_json(snapshot(runtime, tf))
|
||||||
|
|
||||||
async def receive():
|
async def receive():
|
||||||
nonlocal tf
|
nonlocal tf, prefs
|
||||||
while True:
|
while True:
|
||||||
message = await websocket.receive_json()
|
message = await websocket.receive_json()
|
||||||
if message.get("type") == "subscribe":
|
if message.get("type") == "subscribe":
|
||||||
tf = Timeframe(message.get("tf", "1m"))
|
tf = Timeframe(message.get("tf", "1m"))
|
||||||
await websocket.send_json(snapshot(runtime, tf))
|
await websocket.send_json(snapshot(runtime, tf))
|
||||||
|
elif message.get("type") == "prefs":
|
||||||
|
prefs = message
|
||||||
|
|
||||||
receiver = asyncio.create_task(receive())
|
receiver = asyncio.create_task(receive())
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
|
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({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const status = ref({ stream: 'disconnected', bars_held: {} });
|
const status = ref({ stream: 'disconnected', bars_held: {} });
|
||||||
const price = ref(null);
|
const price = ref(null);
|
||||||
const timeframe = ref('1m');
|
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 timeframes = ['1m', '2m', '5m', '15m', '30m', '1h', '4h', '1d'];
|
||||||
const now = ref(Date.now());
|
const now = ref(Date.now());
|
||||||
let chartApi = null;
|
let chartApi = null;
|
||||||
|
|
@ -25,19 +34,24 @@ createApp({
|
||||||
function connect() {
|
function connect() {
|
||||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
socket = new WebSocket(`${protocol}://${location.host}/ws`);
|
socket = new WebSocket(`${protocol}://${location.host}/ws`);
|
||||||
socket.onopen = () => socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value }));
|
socket.onopen = () => {
|
||||||
|
socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value }));
|
||||||
|
sendPrefs();
|
||||||
|
};
|
||||||
socket.onmessage = ({ data }) => {
|
socket.onmessage = ({ data }) => {
|
||||||
const message = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
if (message.type === 'snapshot') {
|
if (message.type === 'snapshot') {
|
||||||
chartApi.setBars(message.bars);
|
chartApi.setBars(message.bars);
|
||||||
chartApi.syncLevels(message.levels || []);
|
levels.value = message.levels || [];
|
||||||
|
syncVisibleLevels();
|
||||||
price.value = message.price;
|
price.value = message.price;
|
||||||
} else if (message.type === 'bar') {
|
} else if (message.type === 'bar') {
|
||||||
chartApi.updateBar(message.bar);
|
chartApi.updateBar(message.bar);
|
||||||
price.value = message.bar.c;
|
price.value = message.bar.c;
|
||||||
status.value.last_bar_t = message.bar.t;
|
status.value.last_bar_t = message.bar.t;
|
||||||
} else if (message.type === 'levels') {
|
} else if (message.type === 'levels') {
|
||||||
chartApi.syncLevels(message.levels);
|
levels.value = message.levels;
|
||||||
|
syncVisibleLevels();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
|
|
@ -48,11 +62,43 @@ createApp({
|
||||||
|
|
||||||
function selectTimeframe(tf) {
|
function selectTimeframe(tf) {
|
||||||
timeframe.value = tf;
|
timeframe.value = tf;
|
||||||
|
prefs.value.base_tf = tf;
|
||||||
if (socket?.readyState === WebSocket.OPEN) {
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({ type: 'subscribe', tf }));
|
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(() => {
|
onMounted(() => {
|
||||||
chartApi = new ConfluenceChart();
|
chartApi = new ConfluenceChart();
|
||||||
chartApi.create(document.getElementById('chart'));
|
chartApi.create(document.getElementById('chart'));
|
||||||
|
|
@ -66,6 +112,6 @@ createApp({
|
||||||
if (chartApi) chartApi.destroy();
|
if (chartApi) chartApi.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
return { status, price, barAge, timeframe, timeframes, selectTimeframe };
|
return { status, price, barAge, timeframe, timeframes, prefs, selectTimeframe, allEnabled, toggleGroup };
|
||||||
},
|
},
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,20 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<aside>
|
<aside>
|
||||||
|
<h2>Layers</h2>
|
||||||
|
<div class="layer-group">
|
||||||
|
<label class="master"><input type="checkbox" :checked="allEnabled('1d')" @change="toggleGroup('1d', $event.target.checked)"><span class="swatch tf-1d"></span>Daily MAs</label>
|
||||||
|
<div class="periods"><label v-for="period in [10,20,50,100,200]" :key="period"><input type="checkbox" :value="period" v-model="prefs.enabled.ma['1d']">{{ period }}</label></div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-group optional">
|
||||||
|
<label><input type="checkbox" :checked="allEnabled('4h')" @change="toggleGroup('4h', $event.target.checked)"><span class="swatch tf-4h"></span>4h MAs</label>
|
||||||
|
<label><input type="checkbox" :checked="allEnabled('1h')" @change="toggleGroup('1h', $event.target.checked)"><span class="swatch tf-1h"></span>1h MAs</label>
|
||||||
|
</div>
|
||||||
|
<div class="layer-group">
|
||||||
|
<label><input type="checkbox" v-model="prefs.enabled.manual"><span class="swatch manual"></span>Manual lines</label>
|
||||||
|
<label class="disabled"><input type="checkbox" disabled>Auto trendlines</label>
|
||||||
|
</div>
|
||||||
|
<label class="score-hidden"><input type="checkbox" v-model="prefs.hidden_levels_score">Hidden levels still count toward confluence</label>
|
||||||
<h2>Confluence zones</h2>
|
<h2>Confluence zones</h2>
|
||||||
<div class="empty">Analysis layers arrive after aggregation.</div>
|
<div class="empty">Analysis layers arrive after aggregation.</div>
|
||||||
<h2>Alert log</h2>
|
<h2>Alert log</h2>
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,5 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted
|
||||||
#chart { height:calc(100vh - 190px); min-height:420px; }
|
#chart { height:calc(100vh - 190px); min-height:420px; }
|
||||||
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }
|
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }
|
||||||
aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; }h2:not(:first-child) { margin-top:30px; }.empty { border-left:2px solid var(--line); padding:10px 12px; color:var(--muted); font-size:11px; }
|
aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; }h2:not(:first-child) { margin-top:30px; }.empty { border-left:2px solid var(--line); padding:10px 12px; color:var(--muted); font-size:11px; }
|
||||||
|
.layer-group { padding:9px 0; border-bottom:1px solid var(--line); display:grid; gap:7px; }.layer-group label,.score-hidden { display:flex; align-items:center; gap:7px; font-size:11px; cursor:pointer; }.layer-group input,.score-hidden input { accent-color:var(--accent); }.periods { display:flex; flex-wrap:wrap; gap:10px; padding-left:22px; }.periods label { color:var(--muted); }.swatch { width:13px; height:3px; display:inline-block; background:var(--muted); }.tf-1d { background:#d96073; }.tf-4h { background:#ec7b42; }.tf-1h { background:#efb643; }.manual { background:#65b7cf; }.optional { color:var(--muted); }.disabled { opacity:.45; }.score-hidden { margin-top:11px; color:var(--muted); line-height:1.25; }
|
||||||
@media (max-width:850px) { #app { padding:10px; }main { grid-template-columns:1fr; }#chart { height:55vh; min-height:360px; }aside { min-height:180px; }header { height:54px; }.chart-head { align-items:flex-start; flex-direction:column; }.timeframes { justify-content:flex-start; }.timeframes button { padding:5px 8px; } }
|
@media (max-width:850px) { #app { padding:10px; }main { grid-template-columns:1fr; }#chart { height:55vh; min-height:360px; }aside { min-height:180px; }header { height:54px; }.chart-head { align-items:flex-start; flex-direction:column; }.timeframes { justify-content:flex-start; }.timeframes button { padding:5px 8px; } }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue