Update app.py
Browse files
app.py
CHANGED
|
@@ -74,7 +74,7 @@ def get_auto_config(date: str, root: str):
|
|
| 74 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 75 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 76 |
|
| 77 |
-
valid_expiries =[e for e in all_expiries if e >= date]
|
| 78 |
if not valid_expiries:
|
| 79 |
valid_expiries = all_expiries
|
| 80 |
|
|
@@ -141,9 +141,9 @@ async def fetch_series(
|
|
| 141 |
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
|
| 142 |
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
|
| 143 |
|
|
|
|
| 144 |
if required_vars is None:
|
| 145 |
required_vars =["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
|
| 146 |
-
|
| 147 |
required_vars = list(set(required_vars) | {"P"})
|
| 148 |
|
| 149 |
if mode == "advanced":
|
|
@@ -156,7 +156,7 @@ async def fetch_series(
|
|
| 156 |
ref_instrument = instrument
|
| 157 |
if not ref_instrument:
|
| 158 |
spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
|
| 159 |
-
fut_keys =
|
| 160 |
if spot_keys:
|
| 161 |
ref_instrument = spot_keys[0]
|
| 162 |
elif fut_keys:
|
|
@@ -165,13 +165,26 @@ async def fetch_series(
|
|
| 165 |
others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
| 166 |
ref_instrument = others[0] if others else instruments[0]
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
rpc_name = "get_advanced_chart_data"
|
| 169 |
base_params = {
|
| 170 |
"p_root": root,
|
| 171 |
"p_expiry": expiry,
|
| 172 |
"p_ref_instrument": ref_instrument,
|
| 173 |
"p_atm_range": atm_range,
|
| 174 |
-
"p_strike_map":
|
| 175 |
}
|
| 176 |
|
| 177 |
else:
|
|
@@ -185,9 +198,7 @@ async def fetch_series(
|
|
| 185 |
"p_instrument": instrument
|
| 186 |
}
|
| 187 |
|
| 188 |
-
#
|
| 189 |
-
# CHUNKING: 2-hour chunks fetched in parallel
|
| 190 |
-
# =========================================================
|
| 191 |
async def fetch_chunk(start_dt, end_dt):
|
| 192 |
params = base_params.copy()
|
| 193 |
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -214,22 +225,13 @@ async def fetch_series(
|
|
| 214 |
if not all_records:
|
| 215 |
return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
|
| 216 |
|
| 217 |
-
#
|
| 218 |
-
# DATA PROCESSING
|
| 219 |
-
# =========================================================
|
| 220 |
df = pd.DataFrame(all_records)
|
| 221 |
|
| 222 |
df.rename(columns={
|
| 223 |
-
"tick_ts": "ts",
|
| 224 |
-
"
|
| 225 |
-
"
|
| 226 |
-
"vol": "V",
|
| 227 |
-
"oi": "OI",
|
| 228 |
-
"cb": "CB",
|
| 229 |
-
"cs": "CS",
|
| 230 |
-
"pb": "PB",
|
| 231 |
-
"ps": "PS",
|
| 232 |
-
"price": "P"
|
| 233 |
}, inplace=True)
|
| 234 |
|
| 235 |
df['ts'] = pd.to_datetime(df['ts'])
|
|
@@ -237,7 +239,6 @@ async def fetch_series(
|
|
| 237 |
df.set_index('ts', inplace=True)
|
| 238 |
df.sort_index(inplace=True)
|
| 239 |
|
| 240 |
-
# Resample
|
| 241 |
tf_map = {
|
| 242 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 243 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
|
@@ -246,28 +247,29 @@ async def fetch_series(
|
|
| 246 |
|
| 247 |
agg_dict = {
|
| 248 |
"P": "last", "V": "sum", "OI": "last", "B": "last", "S": "last",
|
| 249 |
-
"CB": "last", "CS": "last", "PB": "last", "PS": "last",
|
| 250 |
-
"breakdown": "last" # Include the new JSON breakdown
|
| 251 |
}
|
| 252 |
|
| 253 |
existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
|
| 254 |
-
resampled = df.resample(panda_tf).agg(existing_agg)
|
| 255 |
|
| 256 |
-
|
| 257 |
-
"
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
response_data["breakdown"] = cleaned_breakdown
|
| 271 |
|
| 272 |
return JSONResponse(response_data)
|
| 273 |
|
|
@@ -355,7 +357,6 @@ HTML_TEMPLATE = """
|
|
| 355 |
|
| 356 |
.tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
|
| 357 |
.tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
|
| 358 |
-
.tool-btn-small { font-size:10px; padding:2px 6px; height:auto; width:auto; border:1px solid var(--border); }
|
| 359 |
|
| 360 |
.card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
|
| 361 |
.empty-state {
|
|
@@ -391,10 +392,10 @@ HTML_TEMPLATE = """
|
|
| 391 |
box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
|
| 392 |
}
|
| 393 |
.custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
|
| 394 |
-
.custom-dd-opt:last-child { border-bottom: none; }
|
| 395 |
.custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
|
| 396 |
.custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
|
| 397 |
|
|
|
|
| 398 |
.series-row {
|
| 399 |
background: var(--bg-panel); padding: 12px; border-radius: 8px;
|
| 400 |
border: 1px solid var(--border); margin-bottom: 10px;
|
|
@@ -402,62 +403,36 @@ HTML_TEMPLATE = """
|
|
| 402 |
.series-row-top { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
| 403 |
.series-row-bottom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
| 404 |
.series-row-cond { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px; border: 1px dashed var(--border); }
|
| 405 |
-
|
| 406 |
.s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-label-true, .s-label-false {
|
| 407 |
background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
|
| 408 |
border-radius: 6px; padding: 0 10px; height: 30px; font-size: 12px; transition: 0.2s;
|
| 409 |
}
|
| 410 |
-
.
|
| 411 |
-
|
| 412 |
-
.color-input {
|
| 413 |
-
padding: 0; width: 34px; border: 1px solid var(--border); background: var(--bg-input);
|
| 414 |
-
height: 30px; cursor: pointer; border-radius: 6px; overflow: hidden;
|
| 415 |
-
}
|
| 416 |
-
.color-input::-webkit-color-swatch-wrapper { padding: 0; }
|
| 417 |
-
.color-input::-webkit-color-swatch { border: none; }
|
| 418 |
-
.cond-label { font-size: 10px; color: var(--text-secondary); font-weight: 600; white-space: nowrap; }
|
| 419 |
|
| 420 |
-
.segmented-control {
|
| 421 |
-
|
| 422 |
-
border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
|
| 423 |
-
}
|
| 424 |
-
.seg-btn {
|
| 425 |
-
flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600;
|
| 426 |
-
color: var(--text-secondary); cursor: pointer; border: none; background: transparent;
|
| 427 |
-
transition: 0.2s; border-radius: 4px;
|
| 428 |
-
}
|
| 429 |
.seg-btn.active { background: var(--bg-panel); color: var(--accent); }
|
| 430 |
|
| 431 |
-
.info-icon {
|
| 432 |
-
display: inline-flex; align-items: center; justify-content: center;
|
| 433 |
-
width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel);
|
| 434 |
-
color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help;
|
| 435 |
-
border: 1px solid var(--border); margin-left: 8px; transition: 0.2s;
|
| 436 |
-
}
|
| 437 |
-
.info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
|
| 438 |
|
| 439 |
-
/* BREAKDOWN TABLE */
|
| 440 |
-
.breakdown-table {
|
| 441 |
-
|
| 442 |
-
}
|
| 443 |
-
.breakdown-table th {
|
| 444 |
-
background: var(--bg-panel); color: var(--text-secondary); padding: 8px 10px;
|
| 445 |
-
border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0;
|
| 446 |
-
}
|
| 447 |
.breakdown-table th:first-child { text-align: left; }
|
| 448 |
-
.breakdown-table td { padding:
|
| 449 |
.breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
|
| 450 |
-
.breakdown-table tr:hover
|
| 451 |
.val-pos { color: #3fb950; }
|
| 452 |
.val-neg { color: #f85149; }
|
| 453 |
-
.breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height:
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
.
|
| 457 |
-
.sub-table
|
| 458 |
-
.sub-table
|
| 459 |
-
.sub-table
|
| 460 |
-
.
|
|
|
|
| 461 |
</style>
|
| 462 |
</head>
|
| 463 |
<body>
|
|
@@ -530,7 +505,7 @@ HTML_TEMPLATE = """
|
|
| 530 |
<label>Chart Mode</label>
|
| 531 |
<div class="segmented-control" id="cfgModeToggle">
|
| 532 |
<button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
|
| 533 |
-
<button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode
|
| 534 |
</div>
|
| 535 |
</div>
|
| 536 |
|
|
@@ -541,17 +516,14 @@ HTML_TEMPLATE = """
|
|
| 541 |
<div class="input-arrow">▼</div>
|
| 542 |
<div id="dd-cfgInst" class="custom-dd"></div>
|
| 543 |
</div>
|
| 544 |
-
<div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
|
| 545 |
-
* Advanced Mode: select Spot/Future as reference. Aggregates CE/PE strictly N strikes above and N below Spot.
|
| 546 |
-
</div>
|
| 547 |
</div>
|
| 548 |
|
| 549 |
<div id="cfgAdvancedFields" style="display:none;">
|
| 550 |
<div class="form-section" style="margin-top:12px;">
|
| 551 |
-
<label style="color:var(--accent);">
|
| 552 |
<input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
|
| 553 |
<div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
|
| 554 |
-
|
| 555 |
</div>
|
| 556 |
</div>
|
| 557 |
</div>
|
|
@@ -591,7 +563,7 @@ HTML_TEMPLATE = """
|
|
| 591 |
<div class="form-section">
|
| 592 |
<div style="display:flex; align-items:center; margin-bottom:6px;">
|
| 593 |
<label style="margin:0;">Plot Lines</label>
|
| 594 |
-
<span class="info-icon"
|
| 595 |
</div>
|
| 596 |
<div id="cfgSeries"></div>
|
| 597 |
<button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
|
|
@@ -666,7 +638,6 @@ function extractRequiredVars(seriesList) {
|
|
| 666 |
const CustomDD = {
|
| 667 |
data: { cfgRoot:[], cfgExp: [], cfgInst: [] },
|
| 668 |
recent: { cfgRoot:[] },
|
| 669 |
-
|
| 670 |
init() {
|
| 671 |
const saved = localStorage.getItem('dc_recent_symbols');
|
| 672 |
if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
|
|
@@ -704,10 +675,8 @@ const CustomDD = {
|
|
| 704 |
.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
|
| 705 |
.filter(item => item.val.toLowerCase().includes(q));
|
| 706 |
|
| 707 |
-
if (!filtered.length) {
|
| 708 |
-
|
| 709 |
-
return;
|
| 710 |
-
}
|
| 711 |
let html = '', currentGroup = null;
|
| 712 |
filtered.forEach(item => {
|
| 713 |
if (item.group && item.group !== currentGroup) {
|
|
@@ -738,52 +707,14 @@ const CustomDD = {
|
|
| 738 |
// ===================================================
|
| 739 |
const App = {
|
| 740 |
state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
|
| 741 |
-
|
| 742 |
presets: {
|
| 743 |
-
"Basic Flow (Fut/Eq)":[
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
],
|
| 748 |
-
"
|
| 749 |
-
|
| 750 |
-
{ formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
|
| 751 |
-
],
|
| 752 |
-
"Call Flow (Options)":[
|
| 753 |
-
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 754 |
-
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 755 |
-
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 756 |
-
],
|
| 757 |
-
"Put Flow (Options)":[
|
| 758 |
-
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 759 |
-
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 760 |
-
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 761 |
-
],
|
| 762 |
-
"Aggregated Flow (Options)":[
|
| 763 |
-
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 764 |
-
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
|
| 765 |
-
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 766 |
-
],
|
| 767 |
-
"Net Flow (Options)":[
|
| 768 |
-
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 769 |
-
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
|
| 770 |
-
],
|
| 771 |
-
"Price & Flows (Advanced)":[
|
| 772 |
-
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 773 |
-
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
|
| 774 |
-
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 775 |
-
],
|
| 776 |
-
"Net Flow (Advanced)":[
|
| 777 |
-
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 778 |
-
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
|
| 779 |
-
],
|
| 780 |
-
"Components (Advanced)":[
|
| 781 |
-
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 782 |
-
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 783 |
-
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
|
| 784 |
-
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#da3633', cond: 'none' },
|
| 785 |
-
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#f85149', cond: 'none' }
|
| 786 |
-
]
|
| 787 |
},
|
| 788 |
|
| 789 |
async init() {
|
|
@@ -793,41 +724,25 @@ const App = {
|
|
| 793 |
if (savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e) {}
|
| 794 |
|
| 795 |
const saved = localStorage.getItem('dc_pro_v4_state');
|
| 796 |
-
if (saved) {
|
| 797 |
-
|
| 798 |
-
const s = JSON.parse(saved);
|
| 799 |
-
this.state = { ...this.state, ...s };
|
| 800 |
-
} catch(e) { this.initDefault(); }
|
| 801 |
-
} else {
|
| 802 |
-
this.initDefault();
|
| 803 |
-
}
|
| 804 |
|
| 805 |
const dSel = document.getElementById('globalDate');
|
| 806 |
-
if (this.state.date && Array.from(dSel.options).some(o => o.value === this.state.date))
|
| 807 |
-
|
| 808 |
-
else if (dSel.options.length)
|
| 809 |
-
this.state.date = dSel.options[0].value;
|
| 810 |
|
| 811 |
document.getElementById('globalMode').value = this.state.mode;
|
| 812 |
-
this.updateModeUI();
|
| 813 |
-
this.renderGrid();
|
| 814 |
-
this.startLoop();
|
| 815 |
},
|
| 816 |
|
| 817 |
initDefault() {
|
| 818 |
-
this.state.columns = 3;
|
| 819 |
-
|
| 820 |
-
this.state.refreshRate = 15;
|
| 821 |
-
for (let i = 0; i < 3; i++) {
|
| 822 |
-
this.addChartInternal(i);
|
| 823 |
-
this.addChartInternal(i);
|
| 824 |
-
}
|
| 825 |
},
|
| 826 |
|
| 827 |
async loadDates() {
|
| 828 |
try {
|
| 829 |
-
const res = await fetch('/api/dates');
|
| 830 |
-
const dates = await res.json();
|
| 831 |
document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
|
| 832 |
if (!this.state.date && dates.length) this.state.date = dates[0];
|
| 833 |
} catch(e) {}
|
|
@@ -839,6 +754,7 @@ const App = {
|
|
| 839 |
this.state.charts[id] = { config: {}, instance: null, lastData: null };
|
| 840 |
return id;
|
| 841 |
},
|
|
|
|
| 842 |
save() {
|
| 843 |
const cleanCharts = {};
|
| 844 |
for (let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
|
|
@@ -848,57 +764,39 @@ const App = {
|
|
| 848 |
refreshRate: this.state.refreshRate, charts: cleanCharts
|
| 849 |
}));
|
| 850 |
},
|
|
|
|
| 851 |
reset() { if (confirm('Reset Workspace?')) { localStorage.removeItem('dc_pro_v4_state'); location.reload(); } },
|
|
|
|
| 852 |
setLayout(n) {
|
| 853 |
const all = this.state.layout.flat();
|
| 854 |
-
this.state.columns = n;
|
| 855 |
-
this.state.layout = Array.from({ length: n }, () =>[]);
|
| 856 |
all.forEach((id, i) => this.state.layout[i % n].push(id));
|
| 857 |
this.renderGrid(); this.save();
|
| 858 |
},
|
| 859 |
|
| 860 |
renderGrid() {
|
| 861 |
document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i + 1) === this.state.columns));
|
| 862 |
-
const container = document.getElementById('gridRoot');
|
| 863 |
-
container.innerHTML = '';
|
| 864 |
-
|
| 865 |
this.state.layout.forEach((ids, colIdx) => {
|
| 866 |
-
const col = document.createElement('div');
|
| 867 |
-
|
| 868 |
-
const viewH = window.innerHeight - 52;
|
| 869 |
-
const count = ids.length;
|
| 870 |
-
|
| 871 |
ids.forEach(id => {
|
| 872 |
-
if (this.state.charts[id]?.instance) {
|
| 873 |
-
this.state.charts[id].instance.destroy();
|
| 874 |
-
this.state.charts[id].instance = null;
|
| 875 |
-
}
|
| 876 |
const card = this.createCardDOM(id);
|
| 877 |
card.style.height = (count > 0 && count <= 3) ? `calc(${viewH / count}px - 1px)` : '300px';
|
| 878 |
col.appendChild(card);
|
| 879 |
setTimeout(() => ChartLogic.plot(id), 0);
|
| 880 |
});
|
| 881 |
-
|
| 882 |
-
const btn = document.createElement('button');
|
| 883 |
-
btn.className = 'add-chart-btn';
|
| 884 |
-
btn.innerText = '+ Add Chart';
|
| 885 |
btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
|
| 886 |
-
col.appendChild(btn);
|
| 887 |
-
container.appendChild(col);
|
| 888 |
});
|
| 889 |
},
|
| 890 |
|
| 891 |
createCardDOM(id) {
|
| 892 |
-
const div = document.createElement('div');
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
const cfg = this.state.charts[id]?.config || {};
|
| 897 |
-
const isCfg = cfg && cfg.root;
|
| 898 |
-
const modeTag = cfg.chartMode === 'advanced'
|
| 899 |
-
? `<span class="inst-label">2N (±${cfg.atmRange || 5})</span>`
|
| 900 |
-
: `<span class="inst-label">${cfg.instrument || ''}</span>`;
|
| 901 |
-
|
| 902 |
div.innerHTML = `
|
| 903 |
<div class="card-header">
|
| 904 |
<div class="card-info">
|
|
@@ -940,8 +838,7 @@ const App = {
|
|
| 940 |
const loop = () => {
|
| 941 |
if (this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
|
| 942 |
setTimeout(loop, (this.state.refreshRate || 15) * 1000);
|
| 943 |
-
};
|
| 944 |
-
loop();
|
| 945 |
}
|
| 946 |
};
|
| 947 |
|
|
@@ -970,16 +867,9 @@ const ChartLogic = {
|
|
| 970 |
method: 'POST',
|
| 971 |
headers: { 'Content-Type': 'application/json' },
|
| 972 |
body: JSON.stringify({
|
| 973 |
-
date: App.state.date,
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
instrument: cfg.instrument || '',
|
| 977 |
-
timeframe: cfg.timeframe,
|
| 978 |
-
start_time: cfg.start,
|
| 979 |
-
end_time: endTime,
|
| 980 |
-
mode: cfg.chartMode || 'normal',
|
| 981 |
-
atm_range: cfg.atmRange || 5,
|
| 982 |
-
required_vars: requiredVars
|
| 983 |
})
|
| 984 |
});
|
| 985 |
const data = await res.json();
|
|
@@ -989,8 +879,7 @@ const ChartLogic = {
|
|
| 989 |
const b = document.getElementById(`body-${id}`);
|
| 990 |
if (b) b.innerHTML = `<div class="empty-state" style="opacity:0.8;"><span style="font-size:20px;">⚠️</span><br/>${data.error || 'No Data Found'}</div>`;
|
| 991 |
}
|
| 992 |
-
if (dot) dot.classList.remove('retry');
|
| 993 |
-
return;
|
| 994 |
}
|
| 995 |
|
| 996 |
chart.lastData = data;
|
|
@@ -999,15 +888,11 @@ const ChartLogic = {
|
|
| 999 |
if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
|
| 1000 |
|
| 1001 |
const datasets = (cfg.series ||[]).map(s => {
|
| 1002 |
-
const vals = data.labels.map((_, i) => {
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
|
| 1008 |
-
};
|
| 1009 |
-
return evalFormula(s.formula, vars);
|
| 1010 |
-
});
|
| 1011 |
|
| 1012 |
const ds = {
|
| 1013 |
label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0,
|
|
@@ -1016,10 +901,8 @@ const ChartLogic = {
|
|
| 1016 |
|
| 1017 |
if (s.cond && s.cond !== 'none') {
|
| 1018 |
const thresh = parseFloat(s.thresh) || 0;
|
| 1019 |
-
const cTrue = s.color1 || '#238636';
|
| 1020 |
-
const cFalse = s.color2 || '#da3633';
|
| 1021 |
const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
|
| 1022 |
-
|
| 1023 |
ds.segment = { borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse };
|
| 1024 |
ds.borderColor = cTrue; ds.backgroundColor = cTrue;
|
| 1025 |
ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
|
|
@@ -1030,24 +913,19 @@ const ChartLogic = {
|
|
| 1030 |
}
|
| 1031 |
return ds;
|
| 1032 |
});
|
| 1033 |
-
|
| 1034 |
this.render(id, data.labels, datasets, cfg.series ||[]);
|
| 1035 |
|
| 1036 |
} catch(e) { console.error('ChartLogic.plot error:', e); }
|
| 1037 |
},
|
| 1038 |
|
| 1039 |
render(id, labels, datasets, seriesConfig) {
|
| 1040 |
-
const ctx = document.getElementById(`canvas-${id}`);
|
| 1041 |
-
if (!ctx) return;
|
| 1042 |
const chart = App.state.charts[id];
|
| 1043 |
|
| 1044 |
-
if (chart.instance && chart.instance.ctx?.canvas !== ctx) {
|
| 1045 |
-
chart.instance.destroy(); chart.instance = null;
|
| 1046 |
-
}
|
| 1047 |
|
| 1048 |
if (chart.instance) {
|
| 1049 |
-
chart.instance.data.labels = labels;
|
| 1050 |
-
chart.instance.data.datasets = datasets;
|
| 1051 |
chart.instance.update('none');
|
| 1052 |
} else {
|
| 1053 |
const hasRight = datasets.some(d => d.yAxisID === 'y1');
|
|
@@ -1059,15 +937,12 @@ const ChartLogic = {
|
|
| 1059 |
plugins: {
|
| 1060 |
legend: { display: false },
|
| 1061 |
tooltip: {
|
| 1062 |
-
backgroundColor: 'rgba(22,27,34,0.97)',
|
| 1063 |
-
titleColor: '#e6edf3', bodyColor: '#8b949e',
|
| 1064 |
borderColor: '#30363d', borderWidth: 1, padding: 10,
|
| 1065 |
callbacks: {
|
| 1066 |
label: function(context) {
|
| 1067 |
-
const s = seriesConfig[context.datasetIndex];
|
| 1068 |
-
|
| 1069 |
-
const v = context.parsed.y;
|
| 1070 |
-
const fv = v !== null ? v.toFixed(2) : 'N/A';
|
| 1071 |
let condText = '';
|
| 1072 |
if (s.cond && s.cond !== 'none') {
|
| 1073 |
const thresh = parseFloat(s.thresh) || 0;
|
|
@@ -1078,13 +953,11 @@ const ChartLogic = {
|
|
| 1078 |
return `${s.label}: ${fv}${condText}`;
|
| 1079 |
},
|
| 1080 |
labelColor: function(context) {
|
| 1081 |
-
const s = seriesConfig[context.datasetIndex];
|
| 1082 |
-
if (!s) return { borderColor: '#fff', backgroundColor: '#fff' };
|
| 1083 |
let c = s.color1 || '#fff';
|
| 1084 |
if (s.cond && s.cond !== 'none') {
|
| 1085 |
-
const thresh = parseFloat(s.thresh) || 0;
|
| 1086 |
-
const v =
|
| 1087 |
-
c = (s.cond === '>' ? v > thresh : v < thresh) ? (s.color1 || '#238636') : (s.color2 || '#da3633');
|
| 1088 |
}
|
| 1089 |
return { borderColor: c, backgroundColor: c };
|
| 1090 |
}
|
|
@@ -1103,62 +976,42 @@ const ChartLogic = {
|
|
| 1103 |
};
|
| 1104 |
|
| 1105 |
// ===================================================
|
| 1106 |
-
// BREAKDOWN TABLE
|
| 1107 |
// ===================================================
|
| 1108 |
const Breakdown = {
|
| 1109 |
open(id) {
|
| 1110 |
const chart = App.state.charts[id];
|
| 1111 |
-
if (!chart || !chart.lastData || !chart.config.series)
|
| 1112 |
-
alert('No data loaded yet. Please wait for chart to load.');
|
| 1113 |
-
return;
|
| 1114 |
-
}
|
| 1115 |
-
|
| 1116 |
-
const data = chart.lastData;
|
| 1117 |
-
const cfg = chart.config;
|
| 1118 |
-
const series = cfg.series;
|
| 1119 |
-
|
| 1120 |
-
// Check if breakdown JSON is present and populated
|
| 1121 |
-
const hasBreakdown = data.breakdown && data.breakdown.some(b => b && b.length > 0);
|
| 1122 |
|
|
|
|
| 1123 |
const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
|
| 1124 |
const presentVars = allVarKeys.filter(k => data[k] && data[k].some(v => v && v !== 0));
|
| 1125 |
|
| 1126 |
-
// Header
|
| 1127 |
let thead = '<thead><tr><th>Time</th>';
|
| 1128 |
-
if (hasBreakdown) thead += '<th style="text-align:center;">Details</th>';
|
| 1129 |
presentVars.forEach(v => { thead += `<th>${v}</th>`; });
|
| 1130 |
series.forEach(s => { thead += `<th>${s.label}</th>`; });
|
| 1131 |
thead += '</tr></thead>';
|
| 1132 |
|
| 1133 |
-
// Body
|
| 1134 |
let tbody = '<tbody>';
|
| 1135 |
data.labels.forEach((lbl, i) => {
|
| 1136 |
const vars = {
|
| 1137 |
-
P:
|
| 1138 |
CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
|
| 1139 |
-
PS: safeVal(data, 'PS', i), V:
|
| 1140 |
};
|
| 1141 |
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
if (bdArr.length > 0) {
|
| 1147 |
-
tbody += `<td style="text-align:center;"><button class="tool-btn tool-btn-small" onclick="Breakdown.toggleSubRow('${id}', ${i})">▼ Strikes</button></td>`;
|
| 1148 |
-
} else {
|
| 1149 |
-
tbody += `<td></td>`;
|
| 1150 |
-
}
|
| 1151 |
-
}
|
| 1152 |
|
| 1153 |
presentVars.forEach(k => {
|
| 1154 |
-
const v = vars[k];
|
| 1155 |
-
const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
|
| 1156 |
tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
|
| 1157 |
});
|
| 1158 |
|
| 1159 |
series.forEach(s => {
|
| 1160 |
-
const
|
| 1161 |
-
const rv = result !== null ? result : 0;
|
| 1162 |
let cls = rv > 0 ? 'val-pos' : (rv < 0 ? 'val-neg' : '');
|
| 1163 |
let condLabel = '';
|
| 1164 |
if (s.cond && s.cond !== 'none') {
|
|
@@ -1170,62 +1023,47 @@ const Breakdown = {
|
|
| 1170 |
}
|
| 1171 |
tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
|
| 1172 |
});
|
| 1173 |
-
tbody += '</tr>';
|
| 1174 |
|
| 1175 |
-
|
| 1176 |
-
if (hasBreakdown && (data.breakdown[i] || []).length > 0) {
|
| 1177 |
-
const bd = data.breakdown[i];
|
| 1178 |
-
const strikes = {};
|
| 1179 |
-
bd.forEach(item => {
|
| 1180 |
-
if (!strikes[item.strike]) strikes[item.strike] = { CE_B: 0, CE_S: 0, PE_B: 0, PE_S: 0 };
|
| 1181 |
-
if (item.type === 'CE') { strikes[item.strike].CE_B = item.b; strikes[item.strike].CE_S = item.s; }
|
| 1182 |
-
if (item.type === 'PE') { strikes[item.strike].PE_B = item.b; strikes[item.strike].PE_S = item.s; }
|
| 1183 |
-
});
|
| 1184 |
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
});
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
<
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
const
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
<td
|
| 1204 |
-
<td class="val-pos"
|
| 1205 |
-
<td
|
| 1206 |
-
<td class="val-pos" style="text-align:right;">${strikes[k].PE_B.toFixed(2)}</td>
|
| 1207 |
-
<td class="val-neg" style="text-align:right;">${strikes[k].PE_S.toFixed(2)}</td>
|
| 1208 |
</tr>`;
|
| 1209 |
});
|
| 1210 |
subHtml += `</table></div>`;
|
| 1211 |
-
|
| 1212 |
-
const colspan = 1 + 1 + presentVars.length + series.length;
|
| 1213 |
-
tbody += `<tr id="subrow-${id}-${i}" style="display:none; background:var(--bg-header);"><td colspan="${colspan}" style="padding:0;">${subHtml}</td></tr>`;
|
| 1214 |
}
|
| 1215 |
});
|
| 1216 |
tbody += '</tbody>';
|
| 1217 |
|
| 1218 |
-
document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? '
|
| 1219 |
document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
|
| 1220 |
document.getElementById('bdTable').innerHTML = thead + tbody;
|
| 1221 |
document.getElementById('breakdownModal').style.display = 'flex';
|
| 1222 |
},
|
| 1223 |
|
| 1224 |
-
|
| 1225 |
-
const
|
| 1226 |
-
if
|
| 1227 |
-
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
|
| 1228 |
-
}
|
| 1229 |
}
|
| 1230 |
};
|
| 1231 |
|
|
@@ -1242,7 +1080,6 @@ const Config = {
|
|
| 1242 |
btns[1].classList.toggle('active', mode === 'advanced');
|
| 1243 |
document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
|
| 1244 |
document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
|
| 1245 |
-
document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
|
| 1246 |
this.updatePresetsDropdown();
|
| 1247 |
const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
|
| 1248 |
this.loadPreset(defPreset);
|
|
@@ -1253,87 +1090,60 @@ const Config = {
|
|
| 1253 |
},
|
| 1254 |
loadPreset(presetName = null) {
|
| 1255 |
const name = presetName || document.getElementById('cfgPresetsDropdown').value;
|
| 1256 |
-
if (name && App.presets[name]) {
|
| 1257 |
-
document.getElementById('cfgPresetsDropdown').value = name;
|
| 1258 |
-
this.renderSeries(App.presets[name]);
|
| 1259 |
-
}
|
| 1260 |
},
|
| 1261 |
savePreset() {
|
| 1262 |
const name = prompt('Name for this model:', 'Custom Model');
|
| 1263 |
if (!name) return;
|
| 1264 |
App.presets[name] = this.extractSeriesFromDOM();
|
| 1265 |
localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
|
| 1266 |
-
this.updatePresetsDropdown();
|
| 1267 |
-
document.getElementById('cfgPresetsDropdown').value = name;
|
| 1268 |
},
|
| 1269 |
deletePreset() {
|
| 1270 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1271 |
-
const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
|
| 1272 |
-
'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
|
| 1273 |
-
'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
|
| 1274 |
if (locked.includes(name)) return alert('Cannot delete default models.');
|
| 1275 |
-
if (confirm(`Delete model '${name}'?`)) {
|
| 1276 |
-
delete App.presets[name];
|
| 1277 |
-
localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
|
| 1278 |
-
this.updatePresetsDropdown();
|
| 1279 |
-
this.loadPreset();
|
| 1280 |
-
}
|
| 1281 |
},
|
| 1282 |
|
| 1283 |
async open(id) {
|
| 1284 |
this.tid = id; this.isGlobal = false;
|
| 1285 |
-
document.getElementById('configModal').style.display = 'flex';
|
| 1286 |
-
document.getElementById('
|
| 1287 |
-
document.getElementById('
|
| 1288 |
-
document.getElementById('cfgContent').style.display = 'none';
|
| 1289 |
-
document.getElementById('cfgGlobalOnly').style.display = 'none';
|
| 1290 |
-
document.getElementById('cfgChartOnly').style.display = 'block';
|
| 1291 |
-
|
| 1292 |
-
this.updatePresetsDropdown();
|
| 1293 |
|
| 1294 |
-
CustomDD.clear('cfgRoot');
|
| 1295 |
-
const res = await fetch(`/api/roots?date=${App.state.date}`);
|
| 1296 |
-
const roots = await res.json();
|
| 1297 |
CustomDD.set('cfgRoot', roots);
|
| 1298 |
|
| 1299 |
const cfg = App.state.charts[id].config;
|
| 1300 |
-
document.getElementById('cfgTF').value
|
| 1301 |
-
document.getElementById('cfgStart').value = cfg.start
|
| 1302 |
-
document.getElementById('cfgEnd').value
|
| 1303 |
document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
|
| 1304 |
|
| 1305 |
this.currentMode = cfg.chartMode || 'normal';
|
| 1306 |
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
|
| 1307 |
-
btns[0].classList.toggle('active', this.currentMode === 'normal');
|
| 1308 |
-
btns[1].classList.toggle('active', this.currentMode === 'advanced');
|
| 1309 |
document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
|
| 1310 |
document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
|
| 1311 |
-
document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
|
| 1312 |
|
| 1313 |
if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
|
| 1314 |
else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
|
| 1315 |
|
| 1316 |
-
document.getElementById('cfgRoot').value = cfg.root
|
| 1317 |
-
document.getElementById('cfgExp').value = cfg.expiry || '';
|
| 1318 |
-
document.getElementById('cfgInst').value = cfg.instrument || '';
|
| 1319 |
-
|
| 1320 |
if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
|
| 1321 |
-
|
| 1322 |
-
document.getElementById('cfgLoading').style.display = 'none';
|
| 1323 |
-
document.getElementById('cfgContent').style.display = 'block';
|
| 1324 |
},
|
| 1325 |
|
| 1326 |
openDefaults() {
|
| 1327 |
-
this.isGlobal = true;
|
| 1328 |
-
document.getElementById('
|
| 1329 |
-
document.getElementById('
|
| 1330 |
-
document.getElementById('
|
| 1331 |
-
document.getElementById('cfgContent').style.display = 'block';
|
| 1332 |
-
document.getElementById('cfgGlobalOnly').style.display = 'block';
|
| 1333 |
-
document.getElementById('cfgChartOnly').style.display = 'none';
|
| 1334 |
-
document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
|
| 1335 |
this.updatePresetsDropdown();
|
| 1336 |
},
|
|
|
|
| 1337 |
close() { document.getElementById('configModal').style.display = 'none'; },
|
| 1338 |
|
| 1339 |
async onRootChange(preExp, preInst) {
|
|
@@ -1343,81 +1153,50 @@ const Config = {
|
|
| 1343 |
if (!preInst) document.getElementById('cfgInst').value = '';
|
| 1344 |
if (!root) return;
|
| 1345 |
|
| 1346 |
-
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
|
| 1347 |
-
const exps = await res.json();
|
| 1348 |
CustomDD.set('cfgExp', exps);
|
| 1349 |
|
| 1350 |
-
if (preExp && exps.includes(preExp)) {
|
| 1351 |
-
|
| 1352 |
-
await this.onExpChange(preInst);
|
| 1353 |
-
} else if (exps.length > 0) {
|
| 1354 |
const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
|
| 1355 |
-
if (auto.current) {
|
| 1356 |
-
document.getElementById('cfgExp').value = auto.current.expiry;
|
| 1357 |
-
await this.onExpChange(auto.current.instrument);
|
| 1358 |
-
}
|
| 1359 |
}
|
| 1360 |
},
|
| 1361 |
|
| 1362 |
async onExpChange(preInst) {
|
| 1363 |
-
const root = document.getElementById('cfgRoot').value.toUpperCase();
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
if (!root || !exp) return;
|
| 1367 |
-
|
| 1368 |
-
const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
|
| 1369 |
-
const data = await res.json();
|
| 1370 |
if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
|
| 1371 |
if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
|
| 1372 |
if (preInst) document.getElementById('cfgInst').value = preInst;
|
| 1373 |
},
|
| 1374 |
|
| 1375 |
-
toggleCond(sel) {
|
| 1376 |
-
const condGroup = sel.closest('.series-row').querySelector('.series-row-cond');
|
| 1377 |
-
if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex';
|
| 1378 |
-
},
|
| 1379 |
|
| 1380 |
-
renderSeries(list) {
|
| 1381 |
-
const c = document.getElementById('cfgSeries');
|
| 1382 |
-
c.innerHTML = '';
|
| 1383 |
-
list.forEach(s => this._appendSeriesRow(c, s));
|
| 1384 |
-
},
|
| 1385 |
|
| 1386 |
_appendSeriesRow(container, s = {}) {
|
| 1387 |
-
const r = document.createElement('div');
|
| 1388 |
-
r.className = 'series-row';
|
| 1389 |
const showCond = s.cond && s.cond !== 'none';
|
| 1390 |
r.innerHTML = `
|
| 1391 |
<div class="series-row-top">
|
| 1392 |
<input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
|
| 1393 |
<input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
|
| 1394 |
-
<select class="s-axis" style="width:60px;">
|
| 1395 |
-
<option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option>
|
| 1396 |
-
<option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option>
|
| 1397 |
-
</select>
|
| 1398 |
<button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
|
| 1399 |
</div>
|
| 1400 |
<div class="series-row-bottom">
|
| 1401 |
<input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
|
| 1402 |
-
<select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)">
|
| 1403 |
-
<option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option>
|
| 1404 |
-
<option value=">" ${s.cond === '>' ? 'selected' : ''}>If ></option>
|
| 1405 |
-
<option value="<" ${s.cond === '<' ? 'selected' : ''}>If <</option>
|
| 1406 |
-
</select>
|
| 1407 |
</div>
|
| 1408 |
<div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
|
| 1409 |
-
<span class="cond-label">Threshold:</span>
|
| 1410 |
-
<input type="
|
| 1411 |
-
<span class="cond-label">
|
| 1412 |
-
<input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True">
|
| 1413 |
-
<input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
|
| 1414 |
-
<span class="cond-label">False →</span>
|
| 1415 |
-
<input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False">
|
| 1416 |
-
<input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
|
| 1417 |
</div>
|
| 1418 |
`;
|
| 1419 |
-
const solidColor = r.querySelector('.s-color1');
|
| 1420 |
-
const condTrueColor = r.querySelector('.s-color1-cond');
|
| 1421 |
solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
|
| 1422 |
container.appendChild(r);
|
| 1423 |
},
|
|
@@ -1428,50 +1207,27 @@ const Config = {
|
|
| 1428 |
return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
|
| 1429 |
const cond = r.querySelector('.s-cond')?.value || 'none';
|
| 1430 |
return {
|
| 1431 |
-
formula:
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
cond,
|
| 1436 |
-
thresh: r.querySelector('.s-thresh')?.value || '0',
|
| 1437 |
-
color2: r.querySelector('.s-color2')?.value || '#da3633',
|
| 1438 |
-
labelTrue: r.querySelector('.s-label-true')?.value || '',
|
| 1439 |
-
labelFalse: r.querySelector('.s-label-false')?.value || ''
|
| 1440 |
};
|
| 1441 |
});
|
| 1442 |
},
|
| 1443 |
|
| 1444 |
saveChartConfig() {
|
| 1445 |
-
if (this.isGlobal) {
|
| 1446 |
-
|
| 1447 |
-
App.save();
|
| 1448 |
-
this.close();
|
| 1449 |
-
return;
|
| 1450 |
-
}
|
| 1451 |
-
|
| 1452 |
-
const root = document.getElementById('cfgRoot').value.toUpperCase();
|
| 1453 |
-
const exp = document.getElementById('cfgExp').value.toUpperCase();
|
| 1454 |
const inst = document.getElementById('cfgInst').value.toUpperCase();
|
| 1455 |
-
|
| 1456 |
if (!root || !exp) return alert('Please select a Symbol and Expiry.');
|
| 1457 |
-
|
| 1458 |
-
const series = this.extractSeriesFromDOM();
|
| 1459 |
-
if (!series.length) return alert('Add at least one plot line.');
|
| 1460 |
|
| 1461 |
App.state.charts[this.tid].config = {
|
| 1462 |
-
chartMode:
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
timeframe: document.getElementById('cfgTF').value,
|
| 1466 |
-
start: document.getElementById('cfgStart').value,
|
| 1467 |
-
end: document.getElementById('cfgEnd').value,
|
| 1468 |
-
series
|
| 1469 |
};
|
| 1470 |
-
|
| 1471 |
-
App.save();
|
| 1472 |
-
App.renderGrid();
|
| 1473 |
-
ChartLogic.plot(this.tid);
|
| 1474 |
-
this.close();
|
| 1475 |
}
|
| 1476 |
};
|
| 1477 |
|
|
|
|
| 74 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 75 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 76 |
|
| 77 |
+
valid_expiries = [e for e in all_expiries if e >= date]
|
| 78 |
if not valid_expiries:
|
| 79 |
valid_expiries = all_expiries
|
| 80 |
|
|
|
|
| 141 |
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
|
| 142 |
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
|
| 143 |
|
| 144 |
+
# Default: all variables if not specified
|
| 145 |
if required_vars is None:
|
| 146 |
required_vars =["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
|
|
|
|
| 147 |
required_vars = list(set(required_vars) | {"P"})
|
| 148 |
|
| 149 |
if mode == "advanced":
|
|
|
|
| 156 |
ref_instrument = instrument
|
| 157 |
if not ref_instrument:
|
| 158 |
spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
|
| 159 |
+
fut_keys =[k for k in instruments if 'FUT' in k]
|
| 160 |
if spot_keys:
|
| 161 |
ref_instrument = spot_keys[0]
|
| 162 |
elif fut_keys:
|
|
|
|
| 165 |
others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
| 166 |
ref_instrument = others[0] if others else instruments[0]
|
| 167 |
|
| 168 |
+
# PROPER STRIKE EXTRACTION USING PYTHON
|
| 169 |
+
p_strike_map = {}
|
| 170 |
+
for inst in instruments:
|
| 171 |
+
if inst.endswith('CE') or inst.endswith('PE'):
|
| 172 |
+
sym_part = inst.split(':')[-1]
|
| 173 |
+
if sym_part.startswith(root):
|
| 174 |
+
remainder = sym_part[len(root):-2] # Remove root and CE/PE
|
| 175 |
+
if len(remainder) >= 5: # Fyers Expiry dates are 5 characters (e.g. 26302, 26MAR)
|
| 176 |
+
strike_str = remainder[5:]
|
| 177 |
+
try:
|
| 178 |
+
p_strike_map[inst] = float(strike_str)
|
| 179 |
+
except: pass
|
| 180 |
+
|
| 181 |
rpc_name = "get_advanced_chart_data"
|
| 182 |
base_params = {
|
| 183 |
"p_root": root,
|
| 184 |
"p_expiry": expiry,
|
| 185 |
"p_ref_instrument": ref_instrument,
|
| 186 |
"p_atm_range": atm_range,
|
| 187 |
+
"p_strike_map": p_strike_map
|
| 188 |
}
|
| 189 |
|
| 190 |
else:
|
|
|
|
| 198 |
"p_instrument": instrument
|
| 199 |
}
|
| 200 |
|
| 201 |
+
# Chunking requests to DB
|
|
|
|
|
|
|
| 202 |
async def fetch_chunk(start_dt, end_dt):
|
| 203 |
params = base_params.copy()
|
| 204 |
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
| 225 |
if not all_records:
|
| 226 |
return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
|
| 227 |
|
| 228 |
+
# Data Processing
|
|
|
|
|
|
|
| 229 |
df = pd.DataFrame(all_records)
|
| 230 |
|
| 231 |
df.rename(columns={
|
| 232 |
+
"tick_ts": "ts", "b_qty": "B", "s_qty": "S", "vol": "V",
|
| 233 |
+
"oi": "OI", "cb": "CB", "cs": "CS", "pb": "PB", "ps": "PS",
|
| 234 |
+
"price": "P", "details": "details"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}, inplace=True)
|
| 236 |
|
| 237 |
df['ts'] = pd.to_datetime(df['ts'])
|
|
|
|
| 239 |
df.set_index('ts', inplace=True)
|
| 240 |
df.sort_index(inplace=True)
|
| 241 |
|
|
|
|
| 242 |
tf_map = {
|
| 243 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 244 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
|
|
|
| 247 |
|
| 248 |
agg_dict = {
|
| 249 |
"P": "last", "V": "sum", "OI": "last", "B": "last", "S": "last",
|
| 250 |
+
"CB": "last", "CS": "last", "PB": "last", "PS": "last", "details": "last"
|
|
|
|
| 251 |
}
|
| 252 |
|
| 253 |
existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
|
| 254 |
+
resampled = df.resample(panda_tf).agg(existing_agg)
|
| 255 |
|
| 256 |
+
for col in resampled.columns:
|
| 257 |
+
if col == "details":
|
| 258 |
+
resampled[col] = resampled[col].ffill()
|
| 259 |
+
details_list = [d if isinstance(d, list) else None for d in resampled[col]]
|
| 260 |
+
resampled[col] = details_list
|
| 261 |
+
else:
|
| 262 |
+
resampled[col] = resampled[col].ffill().fillna(0)
|
| 263 |
|
| 264 |
+
# Build response payload
|
| 265 |
+
response_data = {"labels": resampled.index.strftime('%H:%M').tolist()}
|
|
|
|
| 266 |
|
| 267 |
+
for var_key in["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS", "details"]:
|
| 268 |
+
if var_key in resampled.columns and var_key in required_vars + ["details"]:
|
| 269 |
+
if var_key == "details":
|
| 270 |
+
response_data[var_key] = resampled[var_key].tolist()
|
| 271 |
+
else:
|
| 272 |
+
response_data[var_key] =[round(float(v), 2) for v in resampled[var_key].tolist()]
|
|
|
|
| 273 |
|
| 274 |
return JSONResponse(response_data)
|
| 275 |
|
|
|
|
| 357 |
|
| 358 |
.tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
|
| 359 |
.tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
|
|
|
|
| 360 |
|
| 361 |
.card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
|
| 362 |
.empty-state {
|
|
|
|
| 392 |
box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
|
| 393 |
}
|
| 394 |
.custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
|
|
|
|
| 395 |
.custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
|
| 396 |
.custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
|
| 397 |
|
| 398 |
+
/* SERIES ROWS */
|
| 399 |
.series-row {
|
| 400 |
background: var(--bg-panel); padding: 12px; border-radius: 8px;
|
| 401 |
border: 1px solid var(--border); margin-bottom: 10px;
|
|
|
|
| 403 |
.series-row-top { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
| 404 |
.series-row-bottom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
| 405 |
.series-row-cond { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px; border: 1px dashed var(--border); }
|
|
|
|
| 406 |
.s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-label-true, .s-label-false {
|
| 407 |
background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
|
| 408 |
border-radius: 6px; padding: 0 10px; height: 30px; font-size: 12px; transition: 0.2s;
|
| 409 |
}
|
| 410 |
+
.color-input { padding: 0; width: 34px; border: 1px solid var(--border); background: var(--bg-input); height: 30px; cursor: pointer; border-radius: 6px; overflow: hidden; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
+
.segmented-control { display: flex; background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px; }
|
| 413 |
+
.seg-btn { flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600; color: var(--text-secondary); cursor: pointer; border: none; background: transparent; transition: 0.2s; border-radius: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
.seg-btn.active { background: var(--bg-panel); color: var(--accent); }
|
| 415 |
|
| 416 |
+
.info-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel); color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help; border: 1px solid var(--border); margin-left: 8px; transition: 0.2s; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
|
| 418 |
+
/* BREAKDOWN TABLE STYLES */
|
| 419 |
+
.breakdown-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; }
|
| 420 |
+
.breakdown-table th { background: var(--bg-panel); color: var(--text-secondary); padding: 6px 10px; border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
.breakdown-table th:first-child { text-align: left; }
|
| 422 |
+
.breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
|
| 423 |
.breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
|
| 424 |
+
.breakdown-table tr:hover td { background: var(--bg-panel); }
|
| 425 |
.val-pos { color: #3fb950; }
|
| 426 |
.val-neg { color: #f85149; }
|
| 427 |
+
.breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
|
| 428 |
+
|
| 429 |
+
.expand-btn { cursor: pointer; color: var(--accent); font-weight: 700; padding: 0 4px; border: 1px solid var(--border); border-radius: 4px; background: transparent; margin-right: 6px; }
|
| 430 |
+
.expand-btn:hover { background: rgba(47,129,247,0.1); }
|
| 431 |
+
.sub-table-row { display: none; background: #0a0d12; border-bottom: 2px solid var(--border); }
|
| 432 |
+
.sub-table { width: 100%; border-collapse: collapse; font-size: 10px; margin: 8px 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden;}
|
| 433 |
+
.sub-table th, .sub-table td { padding: 4px 8px; border: 1px solid #1f242b; text-align: right; }
|
| 434 |
+
.sub-table th { background: #161b22; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px;}
|
| 435 |
+
.sub-table-wrapper { padding: 4px 10px 10px 32px; }
|
| 436 |
</style>
|
| 437 |
</head>
|
| 438 |
<body>
|
|
|
|
| 505 |
<label>Chart Mode</label>
|
| 506 |
<div class="segmented-control" id="cfgModeToggle">
|
| 507 |
<button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
|
| 508 |
+
<button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
|
| 509 |
</div>
|
| 510 |
</div>
|
| 511 |
|
|
|
|
| 516 |
<div class="input-arrow">▼</div>
|
| 517 |
<div id="dd-cfgInst" class="custom-dd"></div>
|
| 518 |
</div>
|
|
|
|
|
|
|
|
|
|
| 519 |
</div>
|
| 520 |
|
| 521 |
<div id="cfgAdvancedFields" style="display:none;">
|
| 522 |
<div class="form-section" style="margin-top:12px;">
|
| 523 |
+
<label style="color:var(--accent);">ATM (+/-) Strike Range</label>
|
| 524 |
<input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
|
| 525 |
<div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
|
| 526 |
+
Strictly Aggregates 1 ATM + N ITM + N OTM options. (e.g., Range 5 = 11 strikes total).
|
| 527 |
</div>
|
| 528 |
</div>
|
| 529 |
</div>
|
|
|
|
| 563 |
<div class="form-section">
|
| 564 |
<div style="display:flex; align-items:center; margin-bottom:6px;">
|
| 565 |
<label style="margin:0;">Plot Lines</label>
|
| 566 |
+
<span class="info-icon" title="Variables: $P=Price, $B=Buy, $S=Sell (Fut/Eq) | $CB=Call Buy, $CS=Call Sell, $PB=Put Buy, $PS=Put Sell (Options)">i</span>
|
| 567 |
</div>
|
| 568 |
<div id="cfgSeries"></div>
|
| 569 |
<button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
|
|
|
|
| 638 |
const CustomDD = {
|
| 639 |
data: { cfgRoot:[], cfgExp: [], cfgInst: [] },
|
| 640 |
recent: { cfgRoot:[] },
|
|
|
|
| 641 |
init() {
|
| 642 |
const saved = localStorage.getItem('dc_recent_symbols');
|
| 643 |
if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
|
|
|
|
| 675 |
.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
|
| 676 |
.filter(item => item.val.toLowerCase().includes(q));
|
| 677 |
|
| 678 |
+
if (!filtered.length) { el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary);">No matches</div>'; return; }
|
| 679 |
+
|
|
|
|
|
|
|
| 680 |
let html = '', currentGroup = null;
|
| 681 |
filtered.forEach(item => {
|
| 682 |
if (item.group && item.group !== currentGroup) {
|
|
|
|
| 707 |
// ===================================================
|
| 708 |
const App = {
|
| 709 |
state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
|
|
|
|
| 710 |
presets: {
|
| 711 |
+
"Basic Flow (Fut/Eq)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
|
| 712 |
+
"Net Flow (Fut/Eq)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }],
|
| 713 |
+
"Call Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
|
| 714 |
+
"Put Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
|
| 715 |
+
"Aggregated Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }],
|
| 716 |
+
"Price & Flows (Advanced)":[{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }],
|
| 717 |
+
"Net Flow (Advanced)":[{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
},
|
| 719 |
|
| 720 |
async init() {
|
|
|
|
| 724 |
if (savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e) {}
|
| 725 |
|
| 726 |
const saved = localStorage.getItem('dc_pro_v4_state');
|
| 727 |
+
if (saved) { try { const s = JSON.parse(saved); this.state = { ...this.state, ...s }; } catch(e) { this.initDefault(); } }
|
| 728 |
+
else this.initDefault();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
|
| 730 |
const dSel = document.getElementById('globalDate');
|
| 731 |
+
if (this.state.date && Array.from(dSel.options).some(o => o.value === this.state.date)) dSel.value = this.state.date;
|
| 732 |
+
else if (dSel.options.length) this.state.date = dSel.options[0].value;
|
|
|
|
|
|
|
| 733 |
|
| 734 |
document.getElementById('globalMode').value = this.state.mode;
|
| 735 |
+
this.updateModeUI(); this.renderGrid(); this.startLoop();
|
|
|
|
|
|
|
| 736 |
},
|
| 737 |
|
| 738 |
initDefault() {
|
| 739 |
+
this.state.columns = 3; this.state.layout = [[], [],[]]; this.state.refreshRate = 15;
|
| 740 |
+
for (let i = 0; i < 3; i++) { this.addChartInternal(i); this.addChartInternal(i); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
},
|
| 742 |
|
| 743 |
async loadDates() {
|
| 744 |
try {
|
| 745 |
+
const res = await fetch('/api/dates'); const dates = await res.json();
|
|
|
|
| 746 |
document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
|
| 747 |
if (!this.state.date && dates.length) this.state.date = dates[0];
|
| 748 |
} catch(e) {}
|
|
|
|
| 754 |
this.state.charts[id] = { config: {}, instance: null, lastData: null };
|
| 755 |
return id;
|
| 756 |
},
|
| 757 |
+
|
| 758 |
save() {
|
| 759 |
const cleanCharts = {};
|
| 760 |
for (let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
|
|
|
|
| 764 |
refreshRate: this.state.refreshRate, charts: cleanCharts
|
| 765 |
}));
|
| 766 |
},
|
| 767 |
+
|
| 768 |
reset() { if (confirm('Reset Workspace?')) { localStorage.removeItem('dc_pro_v4_state'); location.reload(); } },
|
| 769 |
+
|
| 770 |
setLayout(n) {
|
| 771 |
const all = this.state.layout.flat();
|
| 772 |
+
this.state.columns = n; this.state.layout = Array.from({ length: n }, () => []);
|
|
|
|
| 773 |
all.forEach((id, i) => this.state.layout[i % n].push(id));
|
| 774 |
this.renderGrid(); this.save();
|
| 775 |
},
|
| 776 |
|
| 777 |
renderGrid() {
|
| 778 |
document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i + 1) === this.state.columns));
|
| 779 |
+
const container = document.getElementById('gridRoot'); container.innerHTML = '';
|
|
|
|
|
|
|
| 780 |
this.state.layout.forEach((ids, colIdx) => {
|
| 781 |
+
const col = document.createElement('div'); col.className = 'column';
|
| 782 |
+
const viewH = window.innerHeight - 52; const count = ids.length;
|
|
|
|
|
|
|
|
|
|
| 783 |
ids.forEach(id => {
|
| 784 |
+
if (this.state.charts[id]?.instance) { this.state.charts[id].instance.destroy(); this.state.charts[id].instance = null; }
|
|
|
|
|
|
|
|
|
|
| 785 |
const card = this.createCardDOM(id);
|
| 786 |
card.style.height = (count > 0 && count <= 3) ? `calc(${viewH / count}px - 1px)` : '300px';
|
| 787 |
col.appendChild(card);
|
| 788 |
setTimeout(() => ChartLogic.plot(id), 0);
|
| 789 |
});
|
| 790 |
+
const btn = document.createElement('button'); btn.className = 'add-chart-btn'; btn.innerText = '+ Add Chart';
|
|
|
|
|
|
|
|
|
|
| 791 |
btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
|
| 792 |
+
col.appendChild(btn); container.appendChild(col);
|
|
|
|
| 793 |
});
|
| 794 |
},
|
| 795 |
|
| 796 |
createCardDOM(id) {
|
| 797 |
+
const div = document.createElement('div'); div.className = 'chart-card'; div.id = `card-${id}`;
|
| 798 |
+
const cfg = this.state.charts[id]?.config || {}; const isCfg = cfg && cfg.root;
|
| 799 |
+
const modeTag = cfg.chartMode === 'advanced' ? `<span class="inst-label">ATM ± ${cfg.atmRange || 5}</span>` : `<span class="inst-label">${cfg.instrument || ''}</span>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
div.innerHTML = `
|
| 801 |
<div class="card-header">
|
| 802 |
<div class="card-info">
|
|
|
|
| 838 |
const loop = () => {
|
| 839 |
if (this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
|
| 840 |
setTimeout(loop, (this.state.refreshRate || 15) * 1000);
|
| 841 |
+
}; loop();
|
|
|
|
| 842 |
}
|
| 843 |
};
|
| 844 |
|
|
|
|
| 867 |
method: 'POST',
|
| 868 |
headers: { 'Content-Type': 'application/json' },
|
| 869 |
body: JSON.stringify({
|
| 870 |
+
date: App.state.date, root: cfg.root, expiry: cfg.expiry, instrument: cfg.instrument || '',
|
| 871 |
+
timeframe: cfg.timeframe, start_time: cfg.start, end_time: endTime,
|
| 872 |
+
mode: cfg.chartMode || 'normal', atm_range: cfg.atmRange || 5, required_vars: requiredVars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
})
|
| 874 |
});
|
| 875 |
const data = await res.json();
|
|
|
|
| 879 |
const b = document.getElementById(`body-${id}`);
|
| 880 |
if (b) b.innerHTML = `<div class="empty-state" style="opacity:0.8;"><span style="font-size:20px;">⚠️</span><br/>${data.error || 'No Data Found'}</div>`;
|
| 881 |
}
|
| 882 |
+
if (dot) dot.classList.remove('retry'); return;
|
|
|
|
| 883 |
}
|
| 884 |
|
| 885 |
chart.lastData = data;
|
|
|
|
| 888 |
if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
|
| 889 |
|
| 890 |
const datasets = (cfg.series ||[]).map(s => {
|
| 891 |
+
const vals = data.labels.map((_, i) => evalFormula(s.formula, {
|
| 892 |
+
P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
|
| 893 |
+
CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
|
| 894 |
+
PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
|
| 895 |
+
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
|
| 897 |
const ds = {
|
| 898 |
label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0,
|
|
|
|
| 901 |
|
| 902 |
if (s.cond && s.cond !== 'none') {
|
| 903 |
const thresh = parseFloat(s.thresh) || 0;
|
| 904 |
+
const cTrue = s.color1 || '#238636'; const cFalse = s.color2 || '#da3633';
|
|
|
|
| 905 |
const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
|
|
|
|
| 906 |
ds.segment = { borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse };
|
| 907 |
ds.borderColor = cTrue; ds.backgroundColor = cTrue;
|
| 908 |
ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
|
|
|
|
| 913 |
}
|
| 914 |
return ds;
|
| 915 |
});
|
|
|
|
| 916 |
this.render(id, data.labels, datasets, cfg.series ||[]);
|
| 917 |
|
| 918 |
} catch(e) { console.error('ChartLogic.plot error:', e); }
|
| 919 |
},
|
| 920 |
|
| 921 |
render(id, labels, datasets, seriesConfig) {
|
| 922 |
+
const ctx = document.getElementById(`canvas-${id}`); if (!ctx) return;
|
|
|
|
| 923 |
const chart = App.state.charts[id];
|
| 924 |
|
| 925 |
+
if (chart.instance && chart.instance.ctx?.canvas !== ctx) { chart.instance.destroy(); chart.instance = null; }
|
|
|
|
|
|
|
| 926 |
|
| 927 |
if (chart.instance) {
|
| 928 |
+
chart.instance.data.labels = labels; chart.instance.data.datasets = datasets;
|
|
|
|
| 929 |
chart.instance.update('none');
|
| 930 |
} else {
|
| 931 |
const hasRight = datasets.some(d => d.yAxisID === 'y1');
|
|
|
|
| 937 |
plugins: {
|
| 938 |
legend: { display: false },
|
| 939 |
tooltip: {
|
| 940 |
+
backgroundColor: 'rgba(22,27,34,0.97)', titleColor: '#e6edf3', bodyColor: '#8b949e',
|
|
|
|
| 941 |
borderColor: '#30363d', borderWidth: 1, padding: 10,
|
| 942 |
callbacks: {
|
| 943 |
label: function(context) {
|
| 944 |
+
const s = seriesConfig[context.datasetIndex]; if (!s) return '';
|
| 945 |
+
const v = context.parsed.y; const fv = v !== null ? v.toFixed(2) : 'N/A';
|
|
|
|
|
|
|
| 946 |
let condText = '';
|
| 947 |
if (s.cond && s.cond !== 'none') {
|
| 948 |
const thresh = parseFloat(s.thresh) || 0;
|
|
|
|
| 953 |
return `${s.label}: ${fv}${condText}`;
|
| 954 |
},
|
| 955 |
labelColor: function(context) {
|
| 956 |
+
const s = seriesConfig[context.datasetIndex]; if (!s) return { borderColor: '#fff', backgroundColor: '#fff' };
|
|
|
|
| 957 |
let c = s.color1 || '#fff';
|
| 958 |
if (s.cond && s.cond !== 'none') {
|
| 959 |
+
const thresh = parseFloat(s.thresh) || 0; const v = context.parsed.y;
|
| 960 |
+
const isTrue = s.cond === '>' ? v > thresh : v < thresh; c = isTrue ? (s.color1 || '#238636') : (s.color2 || '#da3633');
|
|
|
|
| 961 |
}
|
| 962 |
return { borderColor: c, backgroundColor: c };
|
| 963 |
}
|
|
|
|
| 976 |
};
|
| 977 |
|
| 978 |
// ===================================================
|
| 979 |
+
// BREAKDOWN TABLE
|
| 980 |
// ===================================================
|
| 981 |
const Breakdown = {
|
| 982 |
open(id) {
|
| 983 |
const chart = App.state.charts[id];
|
| 984 |
+
if (!chart || !chart.lastData || !chart.config.series) return alert('No data loaded yet.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 985 |
|
| 986 |
+
const data = chart.lastData; const cfg = chart.config; const series = cfg.series;
|
| 987 |
const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
|
| 988 |
const presentVars = allVarKeys.filter(k => data[k] && data[k].some(v => v && v !== 0));
|
| 989 |
|
|
|
|
| 990 |
let thead = '<thead><tr><th>Time</th>';
|
|
|
|
| 991 |
presentVars.forEach(v => { thead += `<th>${v}</th>`; });
|
| 992 |
series.forEach(s => { thead += `<th>${s.label}</th>`; });
|
| 993 |
thead += '</tr></thead>';
|
| 994 |
|
|
|
|
| 995 |
let tbody = '<tbody>';
|
| 996 |
data.labels.forEach((lbl, i) => {
|
| 997 |
const vars = {
|
| 998 |
+
P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
|
| 999 |
CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
|
| 1000 |
+
PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
|
| 1001 |
};
|
| 1002 |
|
| 1003 |
+
const hasDetails = data.details && data.details[i] && data.details[i] !== null;
|
| 1004 |
+
const expBtn = hasDetails ? `<button class="expand-btn" onclick="Breakdown.toggleRow('sub-${id}-${i}')">+</button>` : '';
|
| 1005 |
+
|
| 1006 |
+
tbody += `<tr><td>${expBtn} ${lbl}</td>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1007 |
|
| 1008 |
presentVars.forEach(k => {
|
| 1009 |
+
const v = vars[k]; const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
|
|
|
|
| 1010 |
tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
|
| 1011 |
});
|
| 1012 |
|
| 1013 |
series.forEach(s => {
|
| 1014 |
+
const rv = evalFormula(s.formula, vars) || 0;
|
|
|
|
| 1015 |
let cls = rv > 0 ? 'val-pos' : (rv < 0 ? 'val-neg' : '');
|
| 1016 |
let condLabel = '';
|
| 1017 |
if (s.cond && s.cond !== 'none') {
|
|
|
|
| 1023 |
}
|
| 1024 |
tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
|
| 1025 |
});
|
|
|
|
| 1026 |
|
| 1027 |
+
tbody += '</tr>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
|
| 1029 |
+
// Individual Sub-Calculation Strike Detail Injection
|
| 1030 |
+
if (hasDetails) {
|
| 1031 |
+
const det = data.details[i];
|
| 1032 |
+
const strikesMap = {};
|
| 1033 |
+
det.forEach(d => {
|
| 1034 |
+
if (!strikesMap[d.k]) strikesMap[d.k] = { CE: null, PE: null };
|
| 1035 |
+
strikesMap[d.k][d.t] = d;
|
| 1036 |
});
|
| 1037 |
+
const sortedStrikes = Object.keys(strikesMap).map(Number).sort((a,b) => a-b);
|
| 1038 |
+
|
| 1039 |
+
let subHtml = `<div class="sub-table-wrapper"><table class="sub-table">
|
| 1040 |
+
<tr><th>STRIKE</th><th>CE BUY</th><th>CE SELL</th><th>PE BUY</th><th>PE SELL</th><th>CE VOL</th><th>PE VOL</th></tr>`;
|
| 1041 |
+
|
| 1042 |
+
sortedStrikes.forEach(k => {
|
| 1043 |
+
const ce = strikesMap[k].CE || {b:0, s:0, v:0};
|
| 1044 |
+
const pe = strikesMap[k].PE || {b:0, s:0, v:0};
|
| 1045 |
+
subHtml += `<tr>
|
| 1046 |
+
<td style="text-align:center; font-weight:bold; color:var(--text-primary); background:#161b22; border-right:1px solid var(--border);">${k}</td>
|
| 1047 |
+
<td class="${ce.b>0?'val-pos':''}">${ce.b}</td><td class="${ce.s>0?'val-neg':''}">${ce.s}</td>
|
| 1048 |
+
<td class="${pe.b>0?'val-pos':''}">${pe.b}</td><td class="${pe.s>0?'val-neg':''}">${pe.s}</td>
|
| 1049 |
+
<td>${ce.v}</td><td>${pe.v}</td>
|
|
|
|
|
|
|
| 1050 |
</tr>`;
|
| 1051 |
});
|
| 1052 |
subHtml += `</table></div>`;
|
| 1053 |
+
tbody += `<tr id="sub-${id}-${i}" class="sub-table-row"><td colspan="100" style="padding:0;">${subHtml}</td></tr>`;
|
|
|
|
|
|
|
| 1054 |
}
|
| 1055 |
});
|
| 1056 |
tbody += '</tbody>';
|
| 1057 |
|
| 1058 |
+
document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
|
| 1059 |
document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
|
| 1060 |
document.getElementById('bdTable').innerHTML = thead + tbody;
|
| 1061 |
document.getElementById('breakdownModal').style.display = 'flex';
|
| 1062 |
},
|
| 1063 |
|
| 1064 |
+
toggleRow(rowId) {
|
| 1065 |
+
const el = document.getElementById(rowId);
|
| 1066 |
+
if(el) el.style.display = el.style.display === 'table-row' ? 'none' : 'table-row';
|
|
|
|
|
|
|
| 1067 |
}
|
| 1068 |
};
|
| 1069 |
|
|
|
|
| 1080 |
btns[1].classList.toggle('active', mode === 'advanced');
|
| 1081 |
document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
|
| 1082 |
document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
|
|
|
|
| 1083 |
this.updatePresetsDropdown();
|
| 1084 |
const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
|
| 1085 |
this.loadPreset(defPreset);
|
|
|
|
| 1090 |
},
|
| 1091 |
loadPreset(presetName = null) {
|
| 1092 |
const name = presetName || document.getElementById('cfgPresetsDropdown').value;
|
| 1093 |
+
if (name && App.presets[name]) { document.getElementById('cfgPresetsDropdown').value = name; this.renderSeries(App.presets[name]); }
|
|
|
|
|
|
|
|
|
|
| 1094 |
},
|
| 1095 |
savePreset() {
|
| 1096 |
const name = prompt('Name for this model:', 'Custom Model');
|
| 1097 |
if (!name) return;
|
| 1098 |
App.presets[name] = this.extractSeriesFromDOM();
|
| 1099 |
localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
|
| 1100 |
+
this.updatePresetsDropdown(); document.getElementById('cfgPresetsDropdown').value = name;
|
|
|
|
| 1101 |
},
|
| 1102 |
deletePreset() {
|
| 1103 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1104 |
+
const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)', 'Put Flow (Options)', 'Aggregated Flow (Options)', 'Price & Flows (Advanced)', 'Net Flow (Advanced)'];
|
|
|
|
|
|
|
| 1105 |
if (locked.includes(name)) return alert('Cannot delete default models.');
|
| 1106 |
+
if (confirm(`Delete model '${name}'?`)) { delete App.presets[name]; localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets)); this.updatePresetsDropdown(); this.loadPreset(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
},
|
| 1108 |
|
| 1109 |
async open(id) {
|
| 1110 |
this.tid = id; this.isGlobal = false;
|
| 1111 |
+
document.getElementById('configModal').style.display = 'flex'; document.getElementById('cfgTitle').innerText = 'Configure Chart';
|
| 1112 |
+
document.getElementById('cfgLoading').style.display = 'block'; document.getElementById('cfgContent').style.display = 'none';
|
| 1113 |
+
document.getElementById('cfgGlobalOnly').style.display = 'none'; document.getElementById('cfgChartOnly').style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
|
| 1115 |
+
this.updatePresetsDropdown(); CustomDD.clear('cfgRoot');
|
| 1116 |
+
const res = await fetch(`/api/roots?date=${App.state.date}`); const roots = await res.json();
|
|
|
|
| 1117 |
CustomDD.set('cfgRoot', roots);
|
| 1118 |
|
| 1119 |
const cfg = App.state.charts[id].config;
|
| 1120 |
+
document.getElementById('cfgTF').value = cfg.timeframe || '1min';
|
| 1121 |
+
document.getElementById('cfgStart').value = cfg.start || '09:15';
|
| 1122 |
+
document.getElementById('cfgEnd').value = cfg.end || '15:30';
|
| 1123 |
document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
|
| 1124 |
|
| 1125 |
this.currentMode = cfg.chartMode || 'normal';
|
| 1126 |
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
|
| 1127 |
+
btns[0].classList.toggle('active', this.currentMode === 'normal'); btns[1].classList.toggle('active', this.currentMode === 'advanced');
|
|
|
|
| 1128 |
document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
|
| 1129 |
document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
|
|
|
|
| 1130 |
|
| 1131 |
if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
|
| 1132 |
else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
|
| 1133 |
|
| 1134 |
+
document.getElementById('cfgRoot').value = cfg.root || ''; document.getElementById('cfgExp').value = cfg.expiry || ''; document.getElementById('cfgInst').value = cfg.instrument || '';
|
|
|
|
|
|
|
|
|
|
| 1135 |
if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
|
| 1136 |
+
document.getElementById('cfgLoading').style.display = 'none'; document.getElementById('cfgContent').style.display = 'block';
|
|
|
|
|
|
|
| 1137 |
},
|
| 1138 |
|
| 1139 |
openDefaults() {
|
| 1140 |
+
this.isGlobal = true; document.getElementById('configModal').style.display = 'flex';
|
| 1141 |
+
document.getElementById('cfgTitle').innerText = 'Global Settings'; document.getElementById('cfgLoading').style.display = 'none';
|
| 1142 |
+
document.getElementById('cfgContent').style.display = 'block'; document.getElementById('cfgGlobalOnly').style.display = 'block';
|
| 1143 |
+
document.getElementById('cfgChartOnly').style.display = 'none'; document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1144 |
this.updatePresetsDropdown();
|
| 1145 |
},
|
| 1146 |
+
|
| 1147 |
close() { document.getElementById('configModal').style.display = 'none'; },
|
| 1148 |
|
| 1149 |
async onRootChange(preExp, preInst) {
|
|
|
|
| 1153 |
if (!preInst) document.getElementById('cfgInst').value = '';
|
| 1154 |
if (!root) return;
|
| 1155 |
|
| 1156 |
+
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`); const exps = await res.json();
|
|
|
|
| 1157 |
CustomDD.set('cfgExp', exps);
|
| 1158 |
|
| 1159 |
+
if (preExp && exps.includes(preExp)) { document.getElementById('cfgExp').value = preExp; await this.onExpChange(preInst); }
|
| 1160 |
+
else if (exps.length > 0) {
|
|
|
|
|
|
|
| 1161 |
const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
|
| 1162 |
+
if (auto.current) { document.getElementById('cfgExp').value = auto.current.expiry; await this.onExpChange(auto.current.instrument); }
|
|
|
|
|
|
|
|
|
|
| 1163 |
}
|
| 1164 |
},
|
| 1165 |
|
| 1166 |
async onExpChange(preInst) {
|
| 1167 |
+
const root = document.getElementById('cfgRoot').value.toUpperCase(); const exp = document.getElementById('cfgExp').value.toUpperCase();
|
| 1168 |
+
CustomDD.clear('cfgInst'); if (!root || !exp) return;
|
| 1169 |
+
const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`); const data = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1170 |
if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
|
| 1171 |
if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
|
| 1172 |
if (preInst) document.getElementById('cfgInst').value = preInst;
|
| 1173 |
},
|
| 1174 |
|
| 1175 |
+
toggleCond(sel) { const condGroup = sel.closest('.series-row').querySelector('.series-row-cond'); if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex'; },
|
|
|
|
|
|
|
|
|
|
| 1176 |
|
| 1177 |
+
renderSeries(list) { const c = document.getElementById('cfgSeries'); c.innerHTML = ''; list.forEach(s => this._appendSeriesRow(c, s)); },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
|
| 1179 |
_appendSeriesRow(container, s = {}) {
|
| 1180 |
+
const r = document.createElement('div'); r.className = 'series-row';
|
|
|
|
| 1181 |
const showCond = s.cond && s.cond !== 'none';
|
| 1182 |
r.innerHTML = `
|
| 1183 |
<div class="series-row-top">
|
| 1184 |
<input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
|
| 1185 |
<input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
|
| 1186 |
+
<select class="s-axis" style="width:60px;"><option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option><option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option></select>
|
|
|
|
|
|
|
|
|
|
| 1187 |
<button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
|
| 1188 |
</div>
|
| 1189 |
<div class="series-row-bottom">
|
| 1190 |
<input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
|
| 1191 |
+
<select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)"><option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option><option value=">" ${s.cond === '>' ? 'selected' : ''}>If ></option><option value="<" ${s.cond === '<' ? 'selected' : ''}>If <</option></select>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
</div>
|
| 1193 |
<div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
|
| 1194 |
+
<span class="cond-label">Threshold:</span><input type="number" class="s-thresh" value="${s.thresh || 0}" style="width:65px;">
|
| 1195 |
+
<span class="cond-label">True →</span><input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True"><input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
|
| 1196 |
+
<span class="cond-label">False →</span><input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False"><input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
</div>
|
| 1198 |
`;
|
| 1199 |
+
const solidColor = r.querySelector('.s-color1'); const condTrueColor = r.querySelector('.s-color1-cond');
|
|
|
|
| 1200 |
solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
|
| 1201 |
container.appendChild(r);
|
| 1202 |
},
|
|
|
|
| 1207 |
return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
|
| 1208 |
const cond = r.querySelector('.s-cond')?.value || 'none';
|
| 1209 |
return {
|
| 1210 |
+
formula: r.querySelector('.s-form')?.value || '$P', label: r.querySelector('.s-lbl')?.value || 'Line', axis: r.querySelector('.s-axis')?.value || 'left',
|
| 1211 |
+
color1: r.querySelector('.s-color1-cond')?.value || r.querySelector('.s-color1')?.value || '#2f81f7', cond,
|
| 1212 |
+
thresh: r.querySelector('.s-thresh')?.value || '0', color2: r.querySelector('.s-color2')?.value || '#da3633',
|
| 1213 |
+
labelTrue: r.querySelector('.s-label-true')?.value || '', labelFalse: r.querySelector('.s-label-false')?.value || ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
};
|
| 1215 |
});
|
| 1216 |
},
|
| 1217 |
|
| 1218 |
saveChartConfig() {
|
| 1219 |
+
if (this.isGlobal) { App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15; App.save(); this.close(); return; }
|
| 1220 |
+
const root = document.getElementById('cfgRoot').value.toUpperCase(); const exp = document.getElementById('cfgExp').value.toUpperCase();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1221 |
const inst = document.getElementById('cfgInst').value.toUpperCase();
|
|
|
|
| 1222 |
if (!root || !exp) return alert('Please select a Symbol and Expiry.');
|
| 1223 |
+
const series = this.extractSeriesFromDOM(); if (!series.length) return alert('Add at least one plot line.');
|
|
|
|
|
|
|
| 1224 |
|
| 1225 |
App.state.charts[this.tid].config = {
|
| 1226 |
+
chartMode: this.currentMode, atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
|
| 1227 |
+
root, expiry: exp, instrument: inst, timeframe: document.getElementById('cfgTF').value,
|
| 1228 |
+
start: document.getElementById('cfgStart').value, end: document.getElementById('cfgEnd').value, series
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
};
|
| 1230 |
+
App.save(); App.renderGrid(); ChartLogic.plot(this.tid); this.close();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
}
|
| 1232 |
};
|
| 1233 |
|