Update app.py
Browse files
app.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
| 5 |
import asyncio
|
| 6 |
import re
|
| 7 |
from datetime import datetime, timedelta
|
| 8 |
-
from fastapi import FastAPI, Body, HTTPException
|
| 9 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from typing import Optional, List, Dict, Any
|
|
@@ -57,10 +57,14 @@ def get_dates():
|
|
| 57 |
return JSONResponse(sorted(list(dates), reverse=True))
|
| 58 |
|
| 59 |
@app.get("/api/roots")
|
| 60 |
-
def get_roots(date: str):
|
| 61 |
-
"""Fetch
|
| 62 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
roots = set()
|
| 65 |
for row in data:
|
| 66 |
if row.get('root'):
|
|
@@ -72,7 +76,7 @@ def get_auto_config(date: str, root: str):
|
|
| 72 |
"""Smart helper for defaults (Current/Next expiry)."""
|
| 73 |
root = root.upper()
|
| 74 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 75 |
-
data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=
|
| 76 |
|
| 77 |
expiry_map = {}
|
| 78 |
for row in data:
|
|
@@ -112,10 +116,11 @@ def get_auto_config(date: str, root: str):
|
|
| 112 |
|
| 113 |
@app.get("/api/expiries")
|
| 114 |
def get_expiries(date: str, root: str):
|
| 115 |
-
"""Fetch
|
| 116 |
root = root.upper()
|
| 117 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 118 |
-
|
|
|
|
| 119 |
exps = set(row['expiry'] for row in data if row.get('expiry'))
|
| 120 |
return JSONResponse(sorted(list(exps)))
|
| 121 |
|
|
@@ -124,7 +129,7 @@ def get_instruments(date: str, root: str, expiry: str):
|
|
| 124 |
"""Fetch instruments and segregate into spot/future vs options."""
|
| 125 |
root = root.upper()
|
| 126 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 127 |
-
data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=
|
| 128 |
instruments = set()
|
| 129 |
for row in data:
|
| 130 |
keys = row.get('data', {}).keys()
|
|
@@ -156,11 +161,6 @@ def fetch_series(
|
|
| 156 |
mode: str = Body("normal"),
|
| 157 |
atm_range: int = Body(5)
|
| 158 |
):
|
| 159 |
-
"""
|
| 160 |
-
Fetches 10s data.
|
| 161 |
-
- Normal mode uses Postgres JSON filtering.
|
| 162 |
-
- Advanced mode fetches minute 'data' dict to calculate ATM strikes dynamically.
|
| 163 |
-
"""
|
| 164 |
root = root.upper()
|
| 165 |
t_start = f"{date}T{start_time}:00"
|
| 166 |
t_end = f"{date}T{end_time}:59"
|
|
@@ -197,7 +197,6 @@ def fetch_series(
|
|
| 197 |
|
| 198 |
sorted_strikes = sorted(list(unique_strikes))
|
| 199 |
|
| 200 |
-
# PER TICK DYNAMIC ATM CALCULATION
|
| 201 |
for i in range(6):
|
| 202 |
p_base = None
|
| 203 |
if base_key and base_key in data_dict:
|
|
@@ -224,7 +223,7 @@ def fetch_series(
|
|
| 224 |
k_data = data_dict[k]
|
| 225 |
bv = k_data.get('b', [0]*6)
|
| 226 |
sv = k_data.get('s', [0]*6)
|
| 227 |
-
vv = k_data.get('v',[0]*6)
|
| 228 |
ov = k_data.get('o', [0]*6)
|
| 229 |
|
| 230 |
b_val = float(bv[i]) if i < len(bv) and bv[i] else 0.0
|
|
@@ -259,7 +258,7 @@ def fetch_series(
|
|
| 259 |
b_arr = inst_data.get('b',[]) or[]
|
| 260 |
s_arr = inst_data.get('s', []) or[]
|
| 261 |
v_arr = inst_data.get('v', []) or[]
|
| 262 |
-
o_arr = inst_data.get('o',[]) or[]
|
| 263 |
|
| 264 |
for i in range(6):
|
| 265 |
if i < len(p_arr) and p_arr[i] is not None:
|
|
@@ -287,14 +286,12 @@ def fetch_series(
|
|
| 287 |
}
|
| 288 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 289 |
|
| 290 |
-
# Advanced Aggregation: Last for price/vol/oi, Mean for dynamic orderbook qtys
|
| 291 |
agg_dict = {
|
| 292 |
"price": "last", "volume": "last", "oi": "last",
|
| 293 |
"buy_qty": "mean", "sell_qty": "mean",
|
| 294 |
"cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
|
| 295 |
}
|
| 296 |
|
| 297 |
-
# Smart gap filling to prevent chart distortion
|
| 298 |
resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
|
| 299 |
|
| 300 |
response_data = {
|
|
@@ -647,11 +644,18 @@ document.addEventListener('click', (e) => {
|
|
| 647 |
|
| 648 |
const CustomDD = {
|
| 649 |
data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
|
|
|
|
| 650 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
clear(id) {
|
| 652 |
this.data[id] =[];
|
| 653 |
document.getElementById(id).value = '';
|
| 654 |
},
|
|
|
|
| 655 |
set(id, items, groupName = null) {
|
| 656 |
if (!groupName) {
|
| 657 |
this.data[id] = items.map(i => ({val: i, group: null}));
|
|
@@ -659,24 +663,50 @@ const CustomDD = {
|
|
| 659 |
this.data[id].push(...items.map(i => ({val: i, group: groupName})));
|
| 660 |
}
|
| 661 |
},
|
| 662 |
-
|
|
|
|
| 663 |
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
|
| 664 |
const inp = document.getElementById(id);
|
| 665 |
inp.select();
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
| 668 |
},
|
| 669 |
-
|
| 670 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
| 672 |
},
|
|
|
|
| 673 |
render(id, query) {
|
| 674 |
const el = document.getElementById(`dd-${id}`);
|
| 675 |
const q = query.toLowerCase();
|
| 676 |
let html = '';
|
| 677 |
let currentGroup = null;
|
| 678 |
|
| 679 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
|
| 681 |
if (filtered.length === 0) {
|
| 682 |
el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
|
|
@@ -692,27 +722,31 @@ const CustomDD = {
|
|
| 692 |
});
|
| 693 |
el.innerHTML = html;
|
| 694 |
},
|
|
|
|
| 695 |
pick(id, val) {
|
| 696 |
document.getElementById(id).value = val;
|
| 697 |
document.getElementById(`dd-${id}`).style.display = 'none';
|
| 698 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
if(id === 'cfgExp') Config.onExpChange();
|
| 700 |
}
|
| 701 |
};
|
| 702 |
-
// ----------------------------------------
|
| 703 |
|
| 704 |
const App = {
|
| 705 |
state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
|
| 706 |
presets: {
|
| 707 |
-
"Standard (Normal Mode)":[
|
| 708 |
-
{ formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
|
| 709 |
-
{ formula: '$B', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
|
| 710 |
-
{ formula: '$S', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
|
| 711 |
-
],
|
| 712 |
"Model 1":[
|
| 713 |
{ formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
|
| 714 |
-
{ formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy
|
| 715 |
-
{ formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell
|
| 716 |
],
|
| 717 |
"Model 2":[
|
| 718 |
{ formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
|
|
@@ -721,6 +755,7 @@ const App = {
|
|
| 721 |
},
|
| 722 |
|
| 723 |
async init() {
|
|
|
|
| 724 |
await this.loadDates();
|
| 725 |
const savedPresets = localStorage.getItem('dc_pro_v3_presets');
|
| 726 |
if(savedPresets) {
|
|
@@ -978,7 +1013,6 @@ const ChartLogic = {
|
|
| 978 |
borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
|
| 979 |
};
|
| 980 |
|
| 981 |
-
// Dynamic Segment Coloring using Chart.js Segment
|
| 982 |
if(s.cond && s.cond !== 'none') {
|
| 983 |
let thresh = parseFloat(s.thresh) || 0;
|
| 984 |
let cTrue = s.color1;
|
|
@@ -986,16 +1020,15 @@ const ChartLogic = {
|
|
| 986 |
|
| 987 |
dataset.segment = {
|
| 988 |
borderColor: ctx => {
|
| 989 |
-
let v = ctx.p1.parsed.y;
|
| 990 |
if(s.cond === '>') return v > thresh ? cTrue : cFalse;
|
| 991 |
if(s.cond === '<') return v < thresh ? cTrue : cFalse;
|
| 992 |
return cTrue;
|
| 993 |
}
|
| 994 |
};
|
| 995 |
-
dataset.borderColor = cTrue;
|
| 996 |
dataset.backgroundColor = cTrue;
|
| 997 |
|
| 998 |
-
// Dynamically calculate the hover color of each point to match the line segment color exactly
|
| 999 |
const hoverColorCallback = (ctx) => {
|
| 1000 |
let v = ctx.raw;
|
| 1001 |
if (v === undefined || v === null) return cTrue;
|
|
@@ -1053,10 +1086,8 @@ const ChartLogic = {
|
|
| 1053 |
let c = s.color1;
|
| 1054 |
if(s.cond && s.cond !== 'none') {
|
| 1055 |
let thresh = parseFloat(s.thresh) || 0;
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
if(s.cond === '>') c = v > thresh ? cTrue : cFalse;
|
| 1059 |
-
if(s.cond === '<') c = v < thresh ? cTrue : cFalse;
|
| 1060 |
}
|
| 1061 |
return { borderColor: c, backgroundColor: c };
|
| 1062 |
}
|
|
@@ -1088,7 +1119,7 @@ const Config = {
|
|
| 1088 |
|
| 1089 |
this.updateInfoTooltip(mode);
|
| 1090 |
this.updatePresetsDropdown();
|
| 1091 |
-
this.loadPreset(
|
| 1092 |
},
|
| 1093 |
|
| 1094 |
updateInfoTooltip(mode) {
|
|
@@ -1130,7 +1161,7 @@ const Config = {
|
|
| 1130 |
|
| 1131 |
deletePreset() {
|
| 1132 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1133 |
-
if(name.startsWith('Model 1') || name.startsWith('Model 2')
|
| 1134 |
if(confirm(`Delete model '${name}'?`)) {
|
| 1135 |
delete App.presets[name];
|
| 1136 |
localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
|
|
@@ -1151,6 +1182,7 @@ const Config = {
|
|
| 1151 |
|
| 1152 |
this.updatePresetsDropdown();
|
| 1153 |
|
|
|
|
| 1154 |
CustomDD.clear('cfgRoot');
|
| 1155 |
const res = await fetch(`/api/roots?date=${App.state.date}`);
|
| 1156 |
const roots = await res.json();
|
|
@@ -1164,7 +1196,6 @@ const Config = {
|
|
| 1164 |
this.currentMode = cfg.chartMode || 'normal';
|
| 1165 |
document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
|
| 1166 |
|
| 1167 |
-
// Setup initial UI states without forcing preset reset if series exist
|
| 1168 |
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
|
| 1169 |
btns[0].classList.toggle('active', this.currentMode === 'normal');
|
| 1170 |
btns[1].classList.toggle('active', this.currentMode === 'advanced');
|
|
@@ -1173,11 +1204,10 @@ const Config = {
|
|
| 1173 |
|
| 1174 |
this.updateInfoTooltip(this.currentMode);
|
| 1175 |
|
| 1176 |
-
// Override defaults if there is already a saved config
|
| 1177 |
if (cfg.series && cfg.series.length > 0) {
|
| 1178 |
this.renderSeries(cfg.series);
|
| 1179 |
} else {
|
| 1180 |
-
this.loadPreset(
|
| 1181 |
}
|
| 1182 |
|
| 1183 |
document.getElementById('cfgRoot').value = cfg.root || '';
|
|
@@ -1201,7 +1231,7 @@ const Config = {
|
|
| 1201 |
document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
|
| 1202 |
|
| 1203 |
this.updatePresetsDropdown();
|
| 1204 |
-
this.loadPreset("
|
| 1205 |
},
|
| 1206 |
|
| 1207 |
close() { document.getElementById('configModal').style.display = 'none'; },
|
|
@@ -1216,6 +1246,7 @@ const Config = {
|
|
| 1216 |
|
| 1217 |
if (!root) return;
|
| 1218 |
|
|
|
|
| 1219 |
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
|
| 1220 |
const exps = await res.json();
|
| 1221 |
CustomDD.set('cfgExp', exps);
|
|
|
|
| 5 |
import asyncio
|
| 6 |
import re
|
| 7 |
from datetime import datetime, timedelta
|
| 8 |
+
from fastapi import FastAPI, Body, HTTPException, Query
|
| 9 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from typing import Optional, List, Dict, Any
|
|
|
|
| 57 |
return JSONResponse(sorted(list(dates), reverse=True))
|
| 58 |
|
| 59 |
@app.get("/api/roots")
|
| 60 |
+
def get_roots(date: str, q: Optional[str] = None):
|
| 61 |
+
"""Fetch roots. If q is provided, search specifically for that string."""
|
| 62 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 63 |
+
endpoint = f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}"
|
| 64 |
+
if q:
|
| 65 |
+
endpoint += f"&root=ilike.*{q.upper()}*"
|
| 66 |
+
|
| 67 |
+
data = sb_get(endpoint)
|
| 68 |
roots = set()
|
| 69 |
for row in data:
|
| 70 |
if row.get('root'):
|
|
|
|
| 76 |
"""Smart helper for defaults (Current/Next expiry)."""
|
| 77 |
root = root.upper()
|
| 78 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 79 |
+
data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
|
| 80 |
|
| 81 |
expiry_map = {}
|
| 82 |
for row in data:
|
|
|
|
| 116 |
|
| 117 |
@app.get("/api/expiries")
|
| 118 |
def get_expiries(date: str, root: str):
|
| 119 |
+
"""Fetch LITERALLY all distinct expiries for a specific root."""
|
| 120 |
root = root.upper()
|
| 121 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 122 |
+
# Note: Removed small limit to ensure all expiries are caught
|
| 123 |
+
data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
|
| 124 |
exps = set(row['expiry'] for row in data if row.get('expiry'))
|
| 125 |
return JSONResponse(sorted(list(exps)))
|
| 126 |
|
|
|
|
| 129 |
"""Fetch instruments and segregate into spot/future vs options."""
|
| 130 |
root = root.upper()
|
| 131 |
start, end = f"{date}T00:00:00", f"{date}T23:59:59"
|
| 132 |
+
data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=50")
|
| 133 |
instruments = set()
|
| 134 |
for row in data:
|
| 135 |
keys = row.get('data', {}).keys()
|
|
|
|
| 161 |
mode: str = Body("normal"),
|
| 162 |
atm_range: int = Body(5)
|
| 163 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
root = root.upper()
|
| 165 |
t_start = f"{date}T{start_time}:00"
|
| 166 |
t_end = f"{date}T{end_time}:59"
|
|
|
|
| 197 |
|
| 198 |
sorted_strikes = sorted(list(unique_strikes))
|
| 199 |
|
|
|
|
| 200 |
for i in range(6):
|
| 201 |
p_base = None
|
| 202 |
if base_key and base_key in data_dict:
|
|
|
|
| 223 |
k_data = data_dict[k]
|
| 224 |
bv = k_data.get('b', [0]*6)
|
| 225 |
sv = k_data.get('s', [0]*6)
|
| 226 |
+
vv = k_data.get('v', [0]*6)
|
| 227 |
ov = k_data.get('o', [0]*6)
|
| 228 |
|
| 229 |
b_val = float(bv[i]) if i < len(bv) and bv[i] else 0.0
|
|
|
|
| 258 |
b_arr = inst_data.get('b',[]) or[]
|
| 259 |
s_arr = inst_data.get('s', []) or[]
|
| 260 |
v_arr = inst_data.get('v', []) or[]
|
| 261 |
+
o_arr = inst_data.get('o', []) or[]
|
| 262 |
|
| 263 |
for i in range(6):
|
| 264 |
if i < len(p_arr) and p_arr[i] is not None:
|
|
|
|
| 286 |
}
|
| 287 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 288 |
|
|
|
|
| 289 |
agg_dict = {
|
| 290 |
"price": "last", "volume": "last", "oi": "last",
|
| 291 |
"buy_qty": "mean", "sell_qty": "mean",
|
| 292 |
"cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
|
| 293 |
}
|
| 294 |
|
|
|
|
| 295 |
resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
|
| 296 |
|
| 297 |
response_data = {
|
|
|
|
| 644 |
|
| 645 |
const CustomDD = {
|
| 646 |
data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
|
| 647 |
+
recent: { cfgRoot: [] }, // Persistent memory for symbols
|
| 648 |
|
| 649 |
+
init() {
|
| 650 |
+
const saved = localStorage.getItem('dc_recent_symbols');
|
| 651 |
+
if(saved) this.recent.cfgRoot = JSON.parse(saved);
|
| 652 |
+
},
|
| 653 |
+
|
| 654 |
clear(id) {
|
| 655 |
this.data[id] =[];
|
| 656 |
document.getElementById(id).value = '';
|
| 657 |
},
|
| 658 |
+
|
| 659 |
set(id, items, groupName = null) {
|
| 660 |
if (!groupName) {
|
| 661 |
this.data[id] = items.map(i => ({val: i, group: null}));
|
|
|
|
| 663 |
this.data[id].push(...items.map(i => ({val: i, group: groupName})));
|
| 664 |
}
|
| 665 |
},
|
| 666 |
+
|
| 667 |
+
async open(id) {
|
| 668 |
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
|
| 669 |
const inp = document.getElementById(id);
|
| 670 |
inp.select();
|
| 671 |
+
|
| 672 |
+
if(id === 'cfgRoot' && inp.value.length === 0) {
|
| 673 |
+
// If empty root box, show popular/recent symbols first
|
| 674 |
+
this.render(id, '');
|
| 675 |
+
} else {
|
| 676 |
+
this.render(id, inp.value);
|
| 677 |
+
}
|
| 678 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
| 679 |
},
|
| 680 |
+
|
| 681 |
+
async filter(id) {
|
| 682 |
+
const query = document.getElementById(id).value.toUpperCase();
|
| 683 |
+
|
| 684 |
+
// Targeted backend search for symbols to minimize egress
|
| 685 |
+
if(id === 'cfgRoot' && query.length >= 2) {
|
| 686 |
+
const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
|
| 687 |
+
const results = await res.json();
|
| 688 |
+
this.set(id, results);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
this.render(id, query);
|
| 692 |
document.getElementById(`dd-${id}`).style.display = 'block';
|
| 693 |
},
|
| 694 |
+
|
| 695 |
render(id, query) {
|
| 696 |
const el = document.getElementById(`dd-${id}`);
|
| 697 |
const q = query.toLowerCase();
|
| 698 |
let html = '';
|
| 699 |
let currentGroup = null;
|
| 700 |
|
| 701 |
+
// Add Recent Symbols to the top of Root list
|
| 702 |
+
let pool = [...this.data[id]];
|
| 703 |
+
if(id === 'cfgRoot' && q === '') {
|
| 704 |
+
const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
|
| 705 |
+
pool = [...recentItems, ...pool];
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i) // Unique
|
| 709 |
+
.filter(item => item.val.toLowerCase().includes(q));
|
| 710 |
|
| 711 |
if (filtered.length === 0) {
|
| 712 |
el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
|
|
|
|
| 722 |
});
|
| 723 |
el.innerHTML = html;
|
| 724 |
},
|
| 725 |
+
|
| 726 |
pick(id, val) {
|
| 727 |
document.getElementById(id).value = val;
|
| 728 |
document.getElementById(`dd-${id}`).style.display = 'none';
|
| 729 |
+
|
| 730 |
+
if(id === 'cfgRoot') {
|
| 731 |
+
// Save to recent
|
| 732 |
+
if(!this.recent.cfgRoot.includes(val)) {
|
| 733 |
+
this.recent.cfgRoot.unshift(val);
|
| 734 |
+
if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
|
| 735 |
+
localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot));
|
| 736 |
+
}
|
| 737 |
+
Config.onRootChange();
|
| 738 |
+
}
|
| 739 |
if(id === 'cfgExp') Config.onExpChange();
|
| 740 |
}
|
| 741 |
};
|
|
|
|
| 742 |
|
| 743 |
const App = {
|
| 744 |
state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
|
| 745 |
presets: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
"Model 1":[
|
| 747 |
{ formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
|
| 748 |
+
{ formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
|
| 749 |
+
{ formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
|
| 750 |
],
|
| 751 |
"Model 2":[
|
| 752 |
{ formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
|
|
|
|
| 755 |
},
|
| 756 |
|
| 757 |
async init() {
|
| 758 |
+
CustomDD.init();
|
| 759 |
await this.loadDates();
|
| 760 |
const savedPresets = localStorage.getItem('dc_pro_v3_presets');
|
| 761 |
if(savedPresets) {
|
|
|
|
| 1013 |
borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
|
| 1014 |
};
|
| 1015 |
|
|
|
|
| 1016 |
if(s.cond && s.cond !== 'none') {
|
| 1017 |
let thresh = parseFloat(s.thresh) || 0;
|
| 1018 |
let cTrue = s.color1;
|
|
|
|
| 1020 |
|
| 1021 |
dataset.segment = {
|
| 1022 |
borderColor: ctx => {
|
| 1023 |
+
let v = ctx.p1.parsed.y;
|
| 1024 |
if(s.cond === '>') return v > thresh ? cTrue : cFalse;
|
| 1025 |
if(s.cond === '<') return v < thresh ? cTrue : cFalse;
|
| 1026 |
return cTrue;
|
| 1027 |
}
|
| 1028 |
};
|
| 1029 |
+
dataset.borderColor = cTrue;
|
| 1030 |
dataset.backgroundColor = cTrue;
|
| 1031 |
|
|
|
|
| 1032 |
const hoverColorCallback = (ctx) => {
|
| 1033 |
let v = ctx.raw;
|
| 1034 |
if (v === undefined || v === null) return cTrue;
|
|
|
|
| 1086 |
let c = s.color1;
|
| 1087 |
if(s.cond && s.cond !== 'none') {
|
| 1088 |
let thresh = parseFloat(s.thresh) || 0;
|
| 1089 |
+
if(s.cond === '>') c = v > thresh ? s.color1 : s.color2;
|
| 1090 |
+
if(s.cond === '<') c = v < thresh ? s.color1 : s.color2;
|
|
|
|
|
|
|
| 1091 |
}
|
| 1092 |
return { borderColor: c, backgroundColor: c };
|
| 1093 |
}
|
|
|
|
| 1119 |
|
| 1120 |
this.updateInfoTooltip(mode);
|
| 1121 |
this.updatePresetsDropdown();
|
| 1122 |
+
this.loadPreset("Model 1"); // Model 1 is now the default
|
| 1123 |
},
|
| 1124 |
|
| 1125 |
updateInfoTooltip(mode) {
|
|
|
|
| 1161 |
|
| 1162 |
deletePreset() {
|
| 1163 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1164 |
+
if(name.startsWith('Model 1') || name.startsWith('Model 2')) return alert("Cannot delete default models.");
|
| 1165 |
if(confirm(`Delete model '${name}'?`)) {
|
| 1166 |
delete App.presets[name];
|
| 1167 |
localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
|
|
|
|
| 1182 |
|
| 1183 |
this.updatePresetsDropdown();
|
| 1184 |
|
| 1185 |
+
// Load existing roots once
|
| 1186 |
CustomDD.clear('cfgRoot');
|
| 1187 |
const res = await fetch(`/api/roots?date=${App.state.date}`);
|
| 1188 |
const roots = await res.json();
|
|
|
|
| 1196 |
this.currentMode = cfg.chartMode || 'normal';
|
| 1197 |
document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
|
| 1198 |
|
|
|
|
| 1199 |
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
|
| 1200 |
btns[0].classList.toggle('active', this.currentMode === 'normal');
|
| 1201 |
btns[1].classList.toggle('active', this.currentMode === 'advanced');
|
|
|
|
| 1204 |
|
| 1205 |
this.updateInfoTooltip(this.currentMode);
|
| 1206 |
|
|
|
|
| 1207 |
if (cfg.series && cfg.series.length > 0) {
|
| 1208 |
this.renderSeries(cfg.series);
|
| 1209 |
} else {
|
| 1210 |
+
this.loadPreset("Model 1");
|
| 1211 |
}
|
| 1212 |
|
| 1213 |
document.getElementById('cfgRoot').value = cfg.root || '';
|
|
|
|
| 1231 |
document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
|
| 1232 |
|
| 1233 |
this.updatePresetsDropdown();
|
| 1234 |
+
this.loadPreset("Model 1");
|
| 1235 |
},
|
| 1236 |
|
| 1237 |
close() { document.getElementById('configModal').style.display = 'none'; },
|
|
|
|
| 1246 |
|
| 1247 |
if (!root) return;
|
| 1248 |
|
| 1249 |
+
// Fetches ALL available expiries for the root
|
| 1250 |
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
|
| 1251 |
const exps = await res.json();
|
| 1252 |
CustomDD.set('cfgExp', exps);
|