mdas-jsview-sdk 1.0.21-uat.0 → 1.0.26-uat.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mdas-sdk.esm.js +1260 -744
- package/dist/mdas-sdk.esm.js.map +1 -1
- package/dist/mdas-sdk.js +1260 -744
- package/dist/mdas-sdk.js.map +1 -1
- package/dist/mdas-sdk.min.js +9 -9
- package/dist/mdas-sdk.min.js.map +1 -1
- package/package.json +1 -1
package/dist/mdas-sdk.js
CHANGED
|
@@ -412,6 +412,85 @@
|
|
|
412
412
|
console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Validate initial symbol with the API and handle access/permission errors
|
|
418
|
+
* This is a reusable method for all widgets that need to validate symbols on initialization
|
|
419
|
+
*
|
|
420
|
+
* @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
|
|
421
|
+
* @param {string} symbol - The symbol to validate
|
|
422
|
+
* @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
|
|
423
|
+
* @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
|
|
424
|
+
*/
|
|
425
|
+
async validateInitialSymbol(apiMethod, symbol) {
|
|
426
|
+
let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
427
|
+
try {
|
|
428
|
+
const apiService = this.wsManager?.getApiService();
|
|
429
|
+
if (!apiService) {
|
|
430
|
+
if (this.debug) {
|
|
431
|
+
console.log(`[${this.constructor.name}] API service not available for initial validation`);
|
|
432
|
+
}
|
|
433
|
+
return true; // Continue without validation
|
|
434
|
+
}
|
|
435
|
+
if (!apiService[apiMethod]) {
|
|
436
|
+
console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
|
|
437
|
+
return true; // Continue without validation
|
|
438
|
+
}
|
|
439
|
+
const result = await apiService[apiMethod](symbol);
|
|
440
|
+
if (result && result.data && result.data.length > 0) {
|
|
441
|
+
if (this.debug) {
|
|
442
|
+
console.log(`[${this.constructor.name}] Initial symbol validated:`, {
|
|
443
|
+
symbol,
|
|
444
|
+
method: apiMethod,
|
|
445
|
+
dataCount: result.data.length
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Call success callback if provided
|
|
450
|
+
if (onSuccess && typeof onSuccess === 'function') {
|
|
451
|
+
onSuccess(result.data, result);
|
|
452
|
+
}
|
|
453
|
+
return true; // Validation successful
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// No data returned - might be access issue, but continue anyway
|
|
457
|
+
if (this.debug) {
|
|
458
|
+
console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
|
|
459
|
+
}
|
|
460
|
+
return true; // Continue anyway, WebSocket might still work
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (this.debug) {
|
|
463
|
+
console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check if it's an access/permission error
|
|
467
|
+
const errorMessage = (error.message || '').toLowerCase();
|
|
468
|
+
const isAccessError = errorMessage.includes('400') ||
|
|
469
|
+
// Bad Request (often used for access issues)
|
|
470
|
+
errorMessage.includes('401') ||
|
|
471
|
+
// Unauthorized
|
|
472
|
+
errorMessage.includes('403') ||
|
|
473
|
+
// Forbidden
|
|
474
|
+
errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
|
|
475
|
+
// Specific OPRA access denial
|
|
476
|
+
errorMessage.includes('permission denied') || errorMessage.includes('access denied');
|
|
477
|
+
if (isAccessError) {
|
|
478
|
+
// Hide loading and show access error
|
|
479
|
+
this.hideLoading();
|
|
480
|
+
|
|
481
|
+
// Extract more specific error message if available
|
|
482
|
+
let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
|
|
483
|
+
if (errorMessage.includes('no opra access')) {
|
|
484
|
+
userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
|
|
485
|
+
}
|
|
486
|
+
this.showError(userMessage);
|
|
487
|
+
return false; // Stop initialization
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// For other errors, continue with WebSocket (might still work)
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
415
494
|
}
|
|
416
495
|
|
|
417
496
|
// src/models/MarketDataModel.js
|
|
@@ -2510,28 +2589,6 @@
|
|
|
2510
2589
|
font-size: 1.71em;
|
|
2511
2590
|
font-weight: 700;
|
|
2512
2591
|
color: #111827;
|
|
2513
|
-
cursor: pointer;
|
|
2514
|
-
transition: all 0.2s ease;
|
|
2515
|
-
position: relative;
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
.time-sales-widget .symbol.editable-symbol:hover {
|
|
2519
|
-
opacity: 0.8;
|
|
2520
|
-
transform: scale(1.02);
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
.time-sales-widget .symbol.editable-symbol:hover::after {
|
|
2524
|
-
content: "✎";
|
|
2525
|
-
position: absolute;
|
|
2526
|
-
top: -8px;
|
|
2527
|
-
right: -8px;
|
|
2528
|
-
background: #3b82f6;
|
|
2529
|
-
color: white;
|
|
2530
|
-
font-size: 10px;
|
|
2531
|
-
padding: 2px 4px;
|
|
2532
|
-
border-radius: 4px;
|
|
2533
|
-
opacity: 0.8;
|
|
2534
|
-
pointer-events: none;
|
|
2535
2592
|
}
|
|
2536
2593
|
|
|
2537
2594
|
/* ========================================
|
|
@@ -2806,8 +2863,36 @@
|
|
|
2806
2863
|
}
|
|
2807
2864
|
`;
|
|
2808
2865
|
|
|
2866
|
+
/**
|
|
2867
|
+
* @deprecated SharedStyles is deprecated and will be removed in a future version.
|
|
2868
|
+
*
|
|
2869
|
+
* Please migrate to BaseStyles + CommonWidgetPatterns:
|
|
2870
|
+
*
|
|
2871
|
+
* BEFORE:
|
|
2872
|
+
* import { SharedStyles } from './styles/index.js';
|
|
2873
|
+
* export const MyWidgetStyles = `${SharedStyles} ...widget styles...`;
|
|
2874
|
+
*
|
|
2875
|
+
* AFTER:
|
|
2876
|
+
* import { BaseStyles } from './styles/BaseStyles.js';
|
|
2877
|
+
* import { getLoadingOverlayStyles, getErrorStyles } from './styles/CommonWidgetPatterns.js';
|
|
2878
|
+
* export const MyWidgetStyles = `
|
|
2879
|
+
* ${BaseStyles}
|
|
2880
|
+
* ${getLoadingOverlayStyles('my-widget')}
|
|
2881
|
+
* ${getErrorStyles('my-widget')}
|
|
2882
|
+
* ...widget-specific styles...
|
|
2883
|
+
* `;
|
|
2884
|
+
*
|
|
2885
|
+
* Benefits of migration:
|
|
2886
|
+
* - Proper CSS scoping (no global conflicts)
|
|
2887
|
+
* - Reduced code duplication
|
|
2888
|
+
* - Better maintainability
|
|
2889
|
+
* - Smaller bundle size
|
|
2890
|
+
*
|
|
2891
|
+
* See STYLING_CUSTOMIZATION_GUIDE.md for details.
|
|
2892
|
+
*/
|
|
2809
2893
|
const SharedStyles = `
|
|
2810
2894
|
/* ========================================
|
|
2895
|
+
⚠️ DEPRECATED - Use BaseStyles.js instead
|
|
2811
2896
|
FONT SIZE SYSTEM - CSS Variables
|
|
2812
2897
|
Adjust --mdas-base-font-size to scale all fonts
|
|
2813
2898
|
======================================== */
|
|
@@ -4483,99 +4568,46 @@
|
|
|
4483
4568
|
this.showLoading();
|
|
4484
4569
|
|
|
4485
4570
|
// Validate initial symbol via API to get company info
|
|
4486
|
-
|
|
4487
|
-
this.
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
const apiService = this.wsManager.getApiService();
|
|
4492
|
-
if (!apiService) {
|
|
4493
|
-
if (this.debug) {
|
|
4494
|
-
console.log('[MarketDataWidget] API service not available for initial validation');
|
|
4495
|
-
}
|
|
4496
|
-
return;
|
|
4571
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
4572
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
4573
|
+
// Store for use in updateWidget
|
|
4574
|
+
if (data && data[0]) {
|
|
4575
|
+
this.initialValidationData = data[0];
|
|
4497
4576
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
if (this.debug) {
|
|
4504
|
-
console.log('[MarketDataWidget] Initial symbol validated:', {
|
|
4505
|
-
symbol: this.symbol,
|
|
4506
|
-
companyName: symbolData.comp_name,
|
|
4507
|
-
exchangeName: symbolData.market_name
|
|
4508
|
-
});
|
|
4509
|
-
}
|
|
4510
|
-
}
|
|
4511
|
-
} catch (error) {
|
|
4512
|
-
if (this.debug) {
|
|
4513
|
-
console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
|
|
4514
|
-
}
|
|
4515
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
4580
|
+
if (validationSuccess === false) {
|
|
4581
|
+
return; // Error is already shown, don't continue
|
|
4516
4582
|
}
|
|
4583
|
+
this.subscribeToData();
|
|
4517
4584
|
}
|
|
4518
4585
|
subscribeToData() {
|
|
4519
4586
|
// Subscribe with symbol for routing
|
|
4520
4587
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
|
|
4521
4588
|
);
|
|
4522
|
-
|
|
4523
|
-
// Send subscription message
|
|
4524
|
-
/* this.wsManager.send({
|
|
4525
|
-
type: 'queryl1',
|
|
4526
|
-
symbol: this.symbol
|
|
4527
|
-
}); */
|
|
4528
4589
|
}
|
|
4590
|
+
handleData(message) {
|
|
4591
|
+
//console.log('DEBUG', message)
|
|
4529
4592
|
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
if (this.debug) {
|
|
4535
|
-
console.log('[MarketDataWidget] Received:', event, data);
|
|
4536
|
-
}
|
|
4537
|
-
if (event === 'connection') {
|
|
4538
|
-
this.handleConnectionStatus(data);
|
|
4539
|
-
return;
|
|
4540
|
-
}
|
|
4541
|
-
if (event === 'data') {
|
|
4542
|
-
this.handleData(data);
|
|
4543
|
-
}
|
|
4544
|
-
if (event === 'session_revoked') {
|
|
4545
|
-
if (data.status === 'attempting_relogin') {
|
|
4546
|
-
this.showLoading();
|
|
4547
|
-
} else if (data.status === 'relogin_failed') {
|
|
4548
|
-
this.showError(data.error);
|
|
4549
|
-
} else if (data.status === 'relogin_successful') {
|
|
4550
|
-
this.hideLoading();
|
|
4551
|
-
}
|
|
4552
|
-
return;
|
|
4553
|
-
}
|
|
4554
|
-
} catch (error) {
|
|
4555
|
-
console.error('[MarketDataWidget] Error handling message:', error);
|
|
4556
|
-
this.showError('Error processing data');
|
|
4593
|
+
// Check for error: false and display message if present
|
|
4594
|
+
if (message.error === true) {
|
|
4595
|
+
if (this.debug) {
|
|
4596
|
+
console.log('[MarketDataWidget] Received error response:', message.message);
|
|
4557
4597
|
}
|
|
4558
|
-
|
|
4598
|
+
const errorMsg = message.message || message.Message;
|
|
4559
4599
|
|
|
4560
|
-
|
|
4561
|
-
if (
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
// Re-send subscription when reconnected
|
|
4566
|
-
this.wsManager.send({
|
|
4567
|
-
type: 'queryl1',
|
|
4568
|
-
symbol: this.symbol
|
|
4569
|
-
});
|
|
4570
|
-
} else if (status.status === 'disconnected') {
|
|
4571
|
-
this.showError('Disconnected from data service');
|
|
4572
|
-
} else if (status.status === 'error') {
|
|
4573
|
-
this.showError(status.error || 'Connection error');
|
|
4600
|
+
// If there's a message field, show it as an error
|
|
4601
|
+
if (errorMsg) {
|
|
4602
|
+
this.hideLoading();
|
|
4603
|
+
this.showError(errorMsg);
|
|
4604
|
+
return;
|
|
4574
4605
|
}
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4606
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
4607
|
+
this.hideLoading();
|
|
4608
|
+
this.showError(message.Message);
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4579
4611
|
if (this.loadingTimeout) {
|
|
4580
4612
|
clearTimeout(this.loadingTimeout);
|
|
4581
4613
|
this.loadingTimeout = null;
|
|
@@ -4661,126 +4693,6 @@
|
|
|
4661
4693
|
this.showConnectionQuality(); // Show cache indicator
|
|
4662
4694
|
}
|
|
4663
4695
|
}
|
|
4664
|
-
showNoDataState() {
|
|
4665
|
-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
4666
|
-
if (this.isDestroyed) return;
|
|
4667
|
-
try {
|
|
4668
|
-
// Hide loading overlay
|
|
4669
|
-
this.hideLoading();
|
|
4670
|
-
|
|
4671
|
-
// Clear any existing errors
|
|
4672
|
-
this.clearError();
|
|
4673
|
-
|
|
4674
|
-
// Update header with dimmed styling and basic info from data
|
|
4675
|
-
const symbol = data.Symbol || this.symbol;
|
|
4676
|
-
|
|
4677
|
-
// Add null checks for all DOM elements
|
|
4678
|
-
const symbolElement = this.container.querySelector('.symbol');
|
|
4679
|
-
if (symbolElement) {
|
|
4680
|
-
symbolElement.textContent = symbol;
|
|
4681
|
-
}
|
|
4682
|
-
const companyNameElement = this.container.querySelector('.company-name');
|
|
4683
|
-
if (companyNameElement) {
|
|
4684
|
-
companyNameElement.textContent = `${symbol} Inc`;
|
|
4685
|
-
}
|
|
4686
|
-
|
|
4687
|
-
// Set price to $0.00 and add dimmed class
|
|
4688
|
-
const currentPriceElement = this.container.querySelector('.current-price');
|
|
4689
|
-
if (currentPriceElement) {
|
|
4690
|
-
currentPriceElement.textContent = '$0.00';
|
|
4691
|
-
}
|
|
4692
|
-
const changeElement = this.container.querySelector('.price-change');
|
|
4693
|
-
if (changeElement) {
|
|
4694
|
-
const changeValueElement = changeElement.querySelector('.change-value');
|
|
4695
|
-
const changePercentElement = changeElement.querySelector('.change-percent');
|
|
4696
|
-
if (changeValueElement) {
|
|
4697
|
-
changeValueElement.textContent = '+0.00';
|
|
4698
|
-
}
|
|
4699
|
-
if (changePercentElement) {
|
|
4700
|
-
changePercentElement.textContent = ' (0.00%)';
|
|
4701
|
-
}
|
|
4702
|
-
changeElement.classList.remove('positive', 'negative');
|
|
4703
|
-
changeElement.classList.add('neutral');
|
|
4704
|
-
}
|
|
4705
|
-
|
|
4706
|
-
// Add dimmed styling to header
|
|
4707
|
-
const widgetHeader = this.container.querySelector('.widget-header');
|
|
4708
|
-
if (widgetHeader) {
|
|
4709
|
-
widgetHeader.classList.add('dimmed');
|
|
4710
|
-
}
|
|
4711
|
-
|
|
4712
|
-
// Replace the data grid with no data message
|
|
4713
|
-
this.showNoDataMessage(symbol, data);
|
|
4714
|
-
|
|
4715
|
-
// Update footer with current timestamp
|
|
4716
|
-
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
4717
|
-
if (lastUpdateElement) {
|
|
4718
|
-
const timestamp = formatTimestampET();
|
|
4719
|
-
lastUpdateElement.textContent = `Checked: ${timestamp}`;
|
|
4720
|
-
}
|
|
4721
|
-
const dataSourceElement = this.container.querySelector('.data-source');
|
|
4722
|
-
if (dataSourceElement) {
|
|
4723
|
-
dataSourceElement.textContent = 'Source: No data available';
|
|
4724
|
-
}
|
|
4725
|
-
} catch (error) {
|
|
4726
|
-
console.error('Error showing no data state:', error);
|
|
4727
|
-
this.showError('Error displaying no data state');
|
|
4728
|
-
}
|
|
4729
|
-
}
|
|
4730
|
-
showNoDataMessage(symbol) {
|
|
4731
|
-
const dataGrid = this.container.querySelector('.data-grid');
|
|
4732
|
-
|
|
4733
|
-
// Hide the data grid if it exists
|
|
4734
|
-
if (dataGrid) {
|
|
4735
|
-
dataGrid.style.display = 'none';
|
|
4736
|
-
}
|
|
4737
|
-
|
|
4738
|
-
// Remove existing no data message if present
|
|
4739
|
-
const existingNoData = this.container.querySelector('.no-data-state');
|
|
4740
|
-
if (existingNoData) {
|
|
4741
|
-
existingNoData.remove();
|
|
4742
|
-
}
|
|
4743
|
-
|
|
4744
|
-
// Create no data message element - safely without XSS risk
|
|
4745
|
-
const noDataElement = createElement('div', '', 'no-data-state');
|
|
4746
|
-
const noDataContent = createElement('div', '', 'no-data-content');
|
|
4747
|
-
|
|
4748
|
-
// Create icon
|
|
4749
|
-
const iconDiv = document.createElement('div');
|
|
4750
|
-
iconDiv.className = 'no-data-icon';
|
|
4751
|
-
iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
4752
|
-
<circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
|
|
4753
|
-
<path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
|
|
4754
|
-
<circle cx="12" cy="16" r="1" fill="#9ca3af"/>
|
|
4755
|
-
</svg>`;
|
|
4756
|
-
noDataContent.appendChild(iconDiv);
|
|
4757
|
-
|
|
4758
|
-
// Create title
|
|
4759
|
-
const title = createElement('h3', 'No Market Data', 'no-data-title');
|
|
4760
|
-
noDataContent.appendChild(title);
|
|
4761
|
-
|
|
4762
|
-
// Create description with sanitized symbol
|
|
4763
|
-
const description = createElement('p', '', 'no-data-description');
|
|
4764
|
-
description.appendChild(document.createTextNode('Market data for '));
|
|
4765
|
-
const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
|
|
4766
|
-
description.appendChild(symbolStrong);
|
|
4767
|
-
description.appendChild(document.createTextNode(' was not found'));
|
|
4768
|
-
noDataContent.appendChild(description);
|
|
4769
|
-
|
|
4770
|
-
// Create guidance
|
|
4771
|
-
const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
|
|
4772
|
-
noDataContent.appendChild(guidance);
|
|
4773
|
-
noDataElement.appendChild(noDataContent);
|
|
4774
|
-
|
|
4775
|
-
// Insert before footer, with null check
|
|
4776
|
-
const footer = this.container.querySelector('.widget-footer');
|
|
4777
|
-
if (footer && footer.parentNode) {
|
|
4778
|
-
footer.parentNode.insertBefore(noDataElement, footer);
|
|
4779
|
-
} else {
|
|
4780
|
-
// Fallback: append to container if footer not found
|
|
4781
|
-
this.container.appendChild(noDataElement);
|
|
4782
|
-
}
|
|
4783
|
-
}
|
|
4784
4696
|
updateWidget(data) {
|
|
4785
4697
|
if (this.isDestroyed) return;
|
|
4786
4698
|
try {
|
|
@@ -5285,7 +5197,20 @@
|
|
|
5285
5197
|
this.showLoading();
|
|
5286
5198
|
|
|
5287
5199
|
// Validate initial symbol via API to get company info
|
|
5288
|
-
|
|
5200
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
5201
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
5202
|
+
// Extract company info from first data item
|
|
5203
|
+
if (data && data[0]) {
|
|
5204
|
+
this.companyName = data[0].comp_name || '';
|
|
5205
|
+
this.exchangeName = data[0].market_name || '';
|
|
5206
|
+
this.mic = data[0].mic || '';
|
|
5207
|
+
}
|
|
5208
|
+
});
|
|
5209
|
+
|
|
5210
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
5211
|
+
if (validationSuccess === false) {
|
|
5212
|
+
return; // Error is already shown, don't continue
|
|
5213
|
+
}
|
|
5289
5214
|
|
|
5290
5215
|
// Set timeout to detect no data on initial load
|
|
5291
5216
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -5302,38 +5227,6 @@
|
|
|
5302
5227
|
|
|
5303
5228
|
this.subscribeToData();
|
|
5304
5229
|
}
|
|
5305
|
-
async validateInitialSymbol() {
|
|
5306
|
-
try {
|
|
5307
|
-
const apiService = this.wsManager.getApiService();
|
|
5308
|
-
if (!apiService) {
|
|
5309
|
-
if (this.debug) {
|
|
5310
|
-
console.log('[NightSessionWidget] API service not available for initial validation');
|
|
5311
|
-
}
|
|
5312
|
-
return;
|
|
5313
|
-
}
|
|
5314
|
-
const result = await apiService.quotel1(this.symbol);
|
|
5315
|
-
if (result && result.data && result.data[0]) {
|
|
5316
|
-
const symbolData = result.data[0];
|
|
5317
|
-
// Extract company info
|
|
5318
|
-
this.companyName = symbolData.comp_name || '';
|
|
5319
|
-
this.exchangeName = symbolData.market_name || '';
|
|
5320
|
-
this.mic = symbolData.mic || '';
|
|
5321
|
-
if (this.debug) {
|
|
5322
|
-
console.log('[NightSessionWidget] Initial symbol validated:', {
|
|
5323
|
-
symbol: this.symbol,
|
|
5324
|
-
companyName: this.companyName,
|
|
5325
|
-
exchangeName: this.exchangeName,
|
|
5326
|
-
mic: this.mic
|
|
5327
|
-
});
|
|
5328
|
-
}
|
|
5329
|
-
}
|
|
5330
|
-
} catch (error) {
|
|
5331
|
-
if (this.debug) {
|
|
5332
|
-
console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
|
|
5333
|
-
}
|
|
5334
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
5230
|
subscribeToData() {
|
|
5338
5231
|
let subscriptionType;
|
|
5339
5232
|
if (this.source === 'bruce') {
|
|
@@ -5353,6 +5246,7 @@
|
|
|
5353
5246
|
}
|
|
5354
5247
|
handleData(message) {
|
|
5355
5248
|
console.log('DEBUG NIGHT', message);
|
|
5249
|
+
|
|
5356
5250
|
//message = message.data || message.Data
|
|
5357
5251
|
|
|
5358
5252
|
if (this.loadingTimeout) {
|
|
@@ -5360,64 +5254,41 @@
|
|
|
5360
5254
|
this.loadingTimeout = null;
|
|
5361
5255
|
}
|
|
5362
5256
|
|
|
5363
|
-
//
|
|
5364
|
-
if (message.
|
|
5257
|
+
// Check for error: false and display message if present
|
|
5258
|
+
if (message.error === true) {
|
|
5365
5259
|
if (this.debug) {
|
|
5366
|
-
console.log('[
|
|
5260
|
+
console.log('[Nigh] Received error response:', message.message);
|
|
5367
5261
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
NotFound: true,
|
|
5373
|
-
message: message.message
|
|
5374
|
-
});
|
|
5375
|
-
} else {
|
|
5262
|
+
const errorMsg = message.message || message.Message;
|
|
5263
|
+
|
|
5264
|
+
// If there's a message field, show it as an error
|
|
5265
|
+
if (errorMsg) {
|
|
5376
5266
|
this.hideLoading();
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
}
|
|
5267
|
+
this.showError(errorMsg);
|
|
5268
|
+
return;
|
|
5380
5269
|
}
|
|
5270
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
5271
|
+
this.hideLoading();
|
|
5272
|
+
this.showError(message.Message);
|
|
5381
5273
|
return;
|
|
5382
5274
|
}
|
|
5383
5275
|
|
|
5384
|
-
// Handle
|
|
5276
|
+
// Handle error messages from server (plain text converted to structured format)
|
|
5385
5277
|
if (message.type === 'error') {
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
this.showNoDataState({
|
|
5393
|
-
Symbol: this.symbol,
|
|
5394
|
-
NotFound: true,
|
|
5395
|
-
isAccessError: true,
|
|
5396
|
-
message: errorMsg
|
|
5397
|
-
});
|
|
5398
|
-
} else {
|
|
5399
|
-
this.hideLoading();
|
|
5400
|
-
if (this.debug) {
|
|
5401
|
-
console.log('[NightSessionWidget] Access error but keeping cached data');
|
|
5402
|
-
}
|
|
5403
|
-
}
|
|
5278
|
+
if (this.debug) {
|
|
5279
|
+
console.log('[NightSessionWidget] Received no data message:', message.message);
|
|
5280
|
+
}
|
|
5281
|
+
// Only show no data state if we don't have cached data
|
|
5282
|
+
if (!this.data) {
|
|
5283
|
+
this.showError(errorMsg);
|
|
5404
5284
|
} else {
|
|
5405
|
-
|
|
5406
|
-
if (
|
|
5407
|
-
console.log('
|
|
5408
|
-
this.showError(errorMsg);
|
|
5409
|
-
} else {
|
|
5410
|
-
this.hideLoading();
|
|
5411
|
-
if (this.debug) {
|
|
5412
|
-
console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
|
|
5413
|
-
}
|
|
5285
|
+
this.hideLoading();
|
|
5286
|
+
if (this.debug) {
|
|
5287
|
+
console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
|
|
5414
5288
|
}
|
|
5415
5289
|
}
|
|
5416
5290
|
return;
|
|
5417
5291
|
}
|
|
5418
|
-
|
|
5419
|
-
// Filter for night session data
|
|
5420
|
-
|
|
5421
5292
|
if (Array.isArray(message.data)) {
|
|
5422
5293
|
// First, try to find data matching our symbol regardless of MarketName
|
|
5423
5294
|
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
@@ -6318,36 +6189,20 @@
|
|
|
6318
6189
|
this.showLoading();
|
|
6319
6190
|
|
|
6320
6191
|
// Validate initial symbol via API to get symbol info
|
|
6321
|
-
|
|
6322
|
-
this.
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
if (!apiService) {
|
|
6328
|
-
if (this.debug) {
|
|
6329
|
-
console.log('[OptionsWidget] API service not available for initial validation');
|
|
6330
|
-
}
|
|
6331
|
-
return;
|
|
6332
|
-
}
|
|
6333
|
-
const result = await apiService.quoteOptionl1(this.symbol);
|
|
6334
|
-
if (result && result[0] && !result[0].error && !result[0].not_found) {
|
|
6335
|
-
const symbolData = result[0];
|
|
6336
|
-
// Store for use in updateWidget
|
|
6337
|
-
this.initialValidationData = symbolData;
|
|
6338
|
-
if (this.debug) {
|
|
6339
|
-
console.log('[OptionsWidget] Initial symbol validated:', {
|
|
6340
|
-
symbol: this.symbol,
|
|
6341
|
-
underlying: symbolData.Underlying || symbolData.RootSymbol
|
|
6342
|
-
});
|
|
6343
|
-
}
|
|
6192
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
6193
|
+
const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
|
|
6194
|
+
// Store for use in updateWidget
|
|
6195
|
+
// Note: quoteOptionl1 returns array directly, not wrapped in data field
|
|
6196
|
+
if (data && data[0] && !data[0].error && !data[0].not_found) {
|
|
6197
|
+
this.initialValidationData = data[0];
|
|
6344
6198
|
}
|
|
6345
|
-
}
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
//
|
|
6199
|
+
});
|
|
6200
|
+
|
|
6201
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
6202
|
+
if (validationSuccess === false) {
|
|
6203
|
+
return; // Error is already shown, don't continue
|
|
6350
6204
|
}
|
|
6205
|
+
this.subscribeToData();
|
|
6351
6206
|
}
|
|
6352
6207
|
subscribeToData() {
|
|
6353
6208
|
// Subscribe with symbol for routing
|
|
@@ -6817,6 +6672,15 @@
|
|
|
6817
6672
|
</select>
|
|
6818
6673
|
<button class="fetch-button" disabled>Search</button>
|
|
6819
6674
|
</div>
|
|
6675
|
+
<div class="filter-section">
|
|
6676
|
+
<label for="strike-filter" class="filter-label">Display:</label>
|
|
6677
|
+
<select class="strike-filter" id="strike-filter">
|
|
6678
|
+
<option value="5">Near the money (±5 strikes)</option>
|
|
6679
|
+
<option value="10">Near the money (±10 strikes)</option>
|
|
6680
|
+
<option value="15">Near the money (±15 strikes)</option>
|
|
6681
|
+
<option value="all">All strikes</option>
|
|
6682
|
+
</select>
|
|
6683
|
+
</div>
|
|
6820
6684
|
</div>
|
|
6821
6685
|
|
|
6822
6686
|
<!-- Data Grid Section -->
|
|
@@ -6873,11 +6737,383 @@
|
|
|
6873
6737
|
</div>
|
|
6874
6738
|
`;
|
|
6875
6739
|
|
|
6740
|
+
// src/widgets/styles/BaseStyles.js
|
|
6741
|
+
|
|
6742
|
+
/**
|
|
6743
|
+
* Base Styles - CSS Variables and Universal Utilities
|
|
6744
|
+
*
|
|
6745
|
+
* This file contains:
|
|
6746
|
+
* 1. CSS Custom Properties (variables) for consistent theming
|
|
6747
|
+
* 2. Responsive font sizing system
|
|
6748
|
+
* 3. Optional utility classes
|
|
6749
|
+
*
|
|
6750
|
+
* Usage:
|
|
6751
|
+
* import { BaseStyles } from './styles/BaseStyles';
|
|
6752
|
+
* export const MyWidgetStyles = `${BaseStyles} ...widget styles...`;
|
|
6753
|
+
*/
|
|
6754
|
+
|
|
6755
|
+
const BaseStyles = `
|
|
6756
|
+
/* ============================================
|
|
6757
|
+
MDAS WIDGET CSS VARIABLES
|
|
6758
|
+
============================================ */
|
|
6759
|
+
|
|
6760
|
+
:root {
|
|
6761
|
+
/* --- FONT SIZE SYSTEM --- */
|
|
6762
|
+
/* Base font size - all other sizes are relative to this */
|
|
6763
|
+
--mdas-base-font-size: 14px;
|
|
6764
|
+
|
|
6765
|
+
/* Component-specific font sizes (em units scale with base) */
|
|
6766
|
+
--mdas-small-text-size: 0.79em; /* 11px at 14px base */
|
|
6767
|
+
--mdas-medium-text-size: 0.93em; /* 13px at 14px base */
|
|
6768
|
+
--mdas-large-text-size: 1.14em; /* 16px at 14px base */
|
|
6769
|
+
|
|
6770
|
+
/* Widget element sizes */
|
|
6771
|
+
--mdas-company-name-size: 1.43em; /* 20px at 14px base */
|
|
6772
|
+
--mdas-symbol-size: 1.79em; /* 25px at 14px base */
|
|
6773
|
+
--mdas-price-size: 2.29em; /* 32px at 14px base */
|
|
6774
|
+
--mdas-change-size: 1.14em; /* 16px at 14px base */
|
|
6775
|
+
|
|
6776
|
+
/* Data display sizes */
|
|
6777
|
+
--mdas-label-size: 0.86em; /* 12px at 14px base */
|
|
6778
|
+
--mdas-value-size: 1em; /* 14px at 14px base */
|
|
6779
|
+
--mdas-footer-size: 0.79em; /* 11px at 14px base */
|
|
6780
|
+
|
|
6781
|
+
/* Chart-specific sizes */
|
|
6782
|
+
--mdas-chart-title-size: 1.29em; /* 18px at 14px base */
|
|
6783
|
+
--mdas-chart-label-size: 0.93em; /* 13px at 14px base */
|
|
6784
|
+
--mdas-chart-value-size: 1.43em; /* 20px at 14px base */
|
|
6785
|
+
|
|
6786
|
+
/* --- COLOR PALETTE (for future theming support) --- */
|
|
6787
|
+
/* Primary colors */
|
|
6788
|
+
--mdas-primary-color: #3b82f6;
|
|
6789
|
+
--mdas-primary-hover: #2563eb;
|
|
6790
|
+
|
|
6791
|
+
/* Status colors */
|
|
6792
|
+
--mdas-color-positive: #059669;
|
|
6793
|
+
--mdas-color-positive-bg: #d1fae5;
|
|
6794
|
+
--mdas-color-negative: #dc2626;
|
|
6795
|
+
--mdas-color-negative-bg: #fee2e2;
|
|
6796
|
+
--mdas-color-neutral: #6b7280;
|
|
6797
|
+
|
|
6798
|
+
/* Background colors */
|
|
6799
|
+
--mdas-bg-primary: #ffffff;
|
|
6800
|
+
--mdas-bg-secondary: #f9fafb;
|
|
6801
|
+
--mdas-bg-tertiary: #f3f4f6;
|
|
6802
|
+
|
|
6803
|
+
/* Text colors */
|
|
6804
|
+
--mdas-text-primary: #111827;
|
|
6805
|
+
--mdas-text-secondary: #6b7280;
|
|
6806
|
+
--mdas-text-tertiary: #9ca3af;
|
|
6807
|
+
|
|
6808
|
+
/* Border colors */
|
|
6809
|
+
--mdas-border-primary: #e5e7eb;
|
|
6810
|
+
--mdas-border-secondary: #d1d5db;
|
|
6811
|
+
|
|
6812
|
+
/* --- SPACING SYSTEM --- */
|
|
6813
|
+
--mdas-spacing-xs: 4px;
|
|
6814
|
+
--mdas-spacing-sm: 8px;
|
|
6815
|
+
--mdas-spacing-md: 16px;
|
|
6816
|
+
--mdas-spacing-lg: 24px;
|
|
6817
|
+
--mdas-spacing-xl: 32px;
|
|
6818
|
+
|
|
6819
|
+
/* --- EFFECTS --- */
|
|
6820
|
+
--mdas-border-radius: 8px;
|
|
6821
|
+
--mdas-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
6822
|
+
--mdas-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
6823
|
+
--mdas-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
|
6824
|
+
|
|
6825
|
+
/* --- Z-INDEX LAYERS --- */
|
|
6826
|
+
--mdas-z-base: 1;
|
|
6827
|
+
--mdas-z-sticky: 10;
|
|
6828
|
+
--mdas-z-overlay: 100;
|
|
6829
|
+
--mdas-z-modal: 10000;
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
/* ============================================
|
|
6833
|
+
RESPONSIVE FONT SCALING
|
|
6834
|
+
============================================ */
|
|
6835
|
+
|
|
6836
|
+
/* Tablet breakpoint - slightly smaller fonts */
|
|
6837
|
+
@media (max-width: 768px) {
|
|
6838
|
+
:root {
|
|
6839
|
+
--mdas-base-font-size: 13px;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
|
|
6843
|
+
/* Mobile breakpoint - smaller fonts for small screens */
|
|
6844
|
+
@media (max-width: 480px) {
|
|
6845
|
+
:root {
|
|
6846
|
+
--mdas-base-font-size: 12px;
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
/* ============================================
|
|
6851
|
+
OPTIONAL UTILITY CLASSES
|
|
6852
|
+
Use these classes for consistent styling
|
|
6853
|
+
============================================ */
|
|
6854
|
+
|
|
6855
|
+
/* Widget card styling - apply to widget root element */
|
|
6856
|
+
.mdas-card {
|
|
6857
|
+
background: var(--mdas-bg-primary);
|
|
6858
|
+
border-radius: var(--mdas-border-radius);
|
|
6859
|
+
padding: var(--mdas-spacing-lg);
|
|
6860
|
+
box-shadow: var(--mdas-shadow-md);
|
|
6861
|
+
border: 1px solid var(--mdas-border-primary);
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
/* Responsive container */
|
|
6865
|
+
.mdas-container {
|
|
6866
|
+
width: 100%;
|
|
6867
|
+
max-width: 1400px;
|
|
6868
|
+
margin: 0 auto;
|
|
6869
|
+
}
|
|
6870
|
+
|
|
6871
|
+
/* Text utilities */
|
|
6872
|
+
.mdas-text-primary {
|
|
6873
|
+
color: var(--mdas-text-primary);
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
.mdas-text-secondary {
|
|
6877
|
+
color: var(--mdas-text-secondary);
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6880
|
+
.mdas-text-muted {
|
|
6881
|
+
color: var(--mdas-text-tertiary);
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
/* Status colors */
|
|
6885
|
+
.mdas-positive {
|
|
6886
|
+
color: var(--mdas-color-positive);
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6889
|
+
.mdas-negative {
|
|
6890
|
+
color: var(--mdas-color-negative);
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
.mdas-neutral {
|
|
6894
|
+
color: var(--mdas-color-neutral);
|
|
6895
|
+
}
|
|
6896
|
+
`;
|
|
6897
|
+
|
|
6898
|
+
// src/widgets/styles/CommonWidgetPatterns.js
|
|
6899
|
+
|
|
6900
|
+
/**
|
|
6901
|
+
* Common Widget Patterns - Reusable Style Functions
|
|
6902
|
+
*
|
|
6903
|
+
* These functions generate properly scoped CSS for common widget patterns.
|
|
6904
|
+
* Each function takes a widget class name and returns scoped CSS.
|
|
6905
|
+
*
|
|
6906
|
+
* Benefits:
|
|
6907
|
+
* - Eliminates code duplication
|
|
6908
|
+
* - Ensures proper scoping (no global conflicts)
|
|
6909
|
+
* - Consistent styling across widgets
|
|
6910
|
+
* - Easy to maintain and update
|
|
6911
|
+
*
|
|
6912
|
+
* Usage:
|
|
6913
|
+
* import { getLoadingOverlayStyles } from './CommonWidgetPatterns';
|
|
6914
|
+
* const styles = `${getLoadingOverlayStyles('my-widget')} ...other styles...`;
|
|
6915
|
+
*/
|
|
6916
|
+
|
|
6917
|
+
/**
|
|
6918
|
+
* Generate loading overlay styles for a widget
|
|
6919
|
+
* @param {string} widgetClass - Widget class name (e.g., 'option-chain-widget')
|
|
6920
|
+
* @returns {string} Scoped CSS for loading overlay
|
|
6921
|
+
*/
|
|
6922
|
+
const getLoadingOverlayStyles = widgetClass => `
|
|
6923
|
+
/* Loading Overlay for ${widgetClass} */
|
|
6924
|
+
.${widgetClass} .widget-loading-overlay {
|
|
6925
|
+
position: absolute;
|
|
6926
|
+
top: 0;
|
|
6927
|
+
left: 0;
|
|
6928
|
+
right: 0;
|
|
6929
|
+
bottom: 0;
|
|
6930
|
+
background: rgba(255, 255, 255, 0.95);
|
|
6931
|
+
backdrop-filter: blur(2px);
|
|
6932
|
+
display: flex;
|
|
6933
|
+
flex-direction: column;
|
|
6934
|
+
align-items: center;
|
|
6935
|
+
justify-content: center;
|
|
6936
|
+
z-index: var(--mdas-z-overlay, 100);
|
|
6937
|
+
border-radius: var(--mdas-border-radius, 12px);
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
.${widgetClass} .widget-loading-overlay.hidden {
|
|
6941
|
+
display: none;
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6944
|
+
.${widgetClass} .loading-content {
|
|
6945
|
+
display: flex;
|
|
6946
|
+
flex-direction: column;
|
|
6947
|
+
align-items: center;
|
|
6948
|
+
gap: 12px;
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
.${widgetClass} .loading-spinner {
|
|
6952
|
+
width: 40px;
|
|
6953
|
+
height: 40px;
|
|
6954
|
+
border: 4px solid var(--mdas-border-primary, #e5e7eb);
|
|
6955
|
+
border-top-color: var(--mdas-primary-color, #3b82f6);
|
|
6956
|
+
border-radius: 50%;
|
|
6957
|
+
animation: ${widgetClass}-spin 1s linear infinite;
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
.${widgetClass} .loading-text {
|
|
6961
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
6962
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
6963
|
+
font-weight: 500;
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
@keyframes ${widgetClass}-spin {
|
|
6967
|
+
to { transform: rotate(360deg); }
|
|
6968
|
+
}
|
|
6969
|
+
`;
|
|
6970
|
+
|
|
6971
|
+
/**
|
|
6972
|
+
* Generate widget error display styles
|
|
6973
|
+
* @param {string} widgetClass - Widget class name
|
|
6974
|
+
* @returns {string} Scoped CSS for error display
|
|
6975
|
+
*/
|
|
6976
|
+
const getErrorStyles = widgetClass => `
|
|
6977
|
+
/* Error Display for ${widgetClass} */
|
|
6978
|
+
.${widgetClass} .widget-error {
|
|
6979
|
+
padding: 16px 20px;
|
|
6980
|
+
background: var(--mdas-color-negative-bg, #fee2e2);
|
|
6981
|
+
border: 1px solid #fecaca;
|
|
6982
|
+
border-radius: var(--mdas-border-radius, 8px);
|
|
6983
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
6984
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
6985
|
+
margin: 16px;
|
|
6986
|
+
text-align: center;
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
.${widgetClass} .widget-error-container {
|
|
6990
|
+
display: flex;
|
|
6991
|
+
flex-direction: column;
|
|
6992
|
+
align-items: center;
|
|
6993
|
+
gap: 12px;
|
|
6994
|
+
padding: 24px;
|
|
6995
|
+
}
|
|
6996
|
+
`;
|
|
6997
|
+
|
|
6998
|
+
/**
|
|
6999
|
+
* Generate widget footer styles
|
|
7000
|
+
* @param {string} widgetClass - Widget class name
|
|
7001
|
+
* @returns {string} Scoped CSS for widget footer
|
|
7002
|
+
*/
|
|
7003
|
+
const getFooterStyles = widgetClass => `
|
|
7004
|
+
/* Footer for ${widgetClass} */
|
|
7005
|
+
.${widgetClass} .widget-footer {
|
|
7006
|
+
display: flex;
|
|
7007
|
+
justify-content: space-between;
|
|
7008
|
+
align-items: center;
|
|
7009
|
+
padding: 8px 16px;
|
|
7010
|
+
background: var(--mdas-bg-secondary, #f9fafb);
|
|
7011
|
+
border-top: 1px solid var(--mdas-border-primary, #e5e7eb);
|
|
7012
|
+
font-size: var(--mdas-footer-size, 0.79em);
|
|
7013
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7014
|
+
}
|
|
7015
|
+
|
|
7016
|
+
.${widgetClass} .widget-footer .last-update {
|
|
7017
|
+
color: var(--mdas-text-tertiary, #9ca3af);
|
|
7018
|
+
}
|
|
7019
|
+
|
|
7020
|
+
.${widgetClass} .widget-footer .data-source {
|
|
7021
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7022
|
+
font-weight: 500;
|
|
7023
|
+
}
|
|
7024
|
+
`;
|
|
7025
|
+
|
|
7026
|
+
/**
|
|
7027
|
+
* Generate no-data state styles
|
|
7028
|
+
* @param {string} widgetClass - Widget class name
|
|
7029
|
+
* @returns {string} Scoped CSS for no-data state
|
|
7030
|
+
*/
|
|
7031
|
+
const getNoDataStyles = widgetClass => `
|
|
7032
|
+
/* No Data State for ${widgetClass} */
|
|
7033
|
+
.${widgetClass} .no-data-state {
|
|
7034
|
+
display: flex;
|
|
7035
|
+
flex-direction: column;
|
|
7036
|
+
align-items: center;
|
|
7037
|
+
justify-content: center;
|
|
7038
|
+
padding: 40px 20px;
|
|
7039
|
+
text-align: center;
|
|
7040
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7041
|
+
}
|
|
7042
|
+
|
|
7043
|
+
.${widgetClass} .no-data-content {
|
|
7044
|
+
max-width: 400px;
|
|
7045
|
+
display: flex;
|
|
7046
|
+
flex-direction: column;
|
|
7047
|
+
align-items: center;
|
|
7048
|
+
gap: 16px;
|
|
7049
|
+
}
|
|
7050
|
+
|
|
7051
|
+
.${widgetClass} .no-data-icon {
|
|
7052
|
+
width: 64px;
|
|
7053
|
+
height: 64px;
|
|
7054
|
+
display: flex;
|
|
7055
|
+
align-items: center;
|
|
7056
|
+
justify-content: center;
|
|
7057
|
+
opacity: 0.5;
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
.${widgetClass} .no-data-icon svg {
|
|
7061
|
+
width: 100%;
|
|
7062
|
+
height: 100%;
|
|
7063
|
+
}
|
|
7064
|
+
|
|
7065
|
+
.${widgetClass} .no-data-title {
|
|
7066
|
+
font-size: var(--mdas-large-text-size, 1.14em);
|
|
7067
|
+
font-weight: 600;
|
|
7068
|
+
color: var(--mdas-text-primary, #111827);
|
|
7069
|
+
margin: 0;
|
|
7070
|
+
}
|
|
7071
|
+
|
|
7072
|
+
.${widgetClass} .no-data-description {
|
|
7073
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
7074
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7075
|
+
margin: 0;
|
|
7076
|
+
line-height: 1.5;
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
.${widgetClass} .no-data-description strong {
|
|
7080
|
+
color: var(--mdas-text-primary, #111827);
|
|
7081
|
+
font-weight: 600;
|
|
7082
|
+
}
|
|
7083
|
+
|
|
7084
|
+
.${widgetClass} .no-data-guidance {
|
|
7085
|
+
font-size: var(--mdas-small-text-size, 0.79em);
|
|
7086
|
+
color: var(--mdas-text-tertiary, #9ca3af);
|
|
7087
|
+
margin: 0;
|
|
7088
|
+
}
|
|
7089
|
+
|
|
7090
|
+
/* Error variant of no-data state */
|
|
7091
|
+
.${widgetClass} .no-data-state.error-access {
|
|
7092
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
.${widgetClass} .no-data-state.error-access .no-data-title {
|
|
7096
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
.${widgetClass} .no-data-state.error-access .no-data-icon {
|
|
7100
|
+
opacity: 0.8;
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
.${widgetClass} .no-data-state.error-access .no-data-icon svg circle[fill] {
|
|
7104
|
+
fill: var(--mdas-color-negative, #dc2626);
|
|
7105
|
+
}
|
|
7106
|
+
`;
|
|
7107
|
+
|
|
6876
7108
|
// src/widgets/styles/OptionChainStyles.js
|
|
6877
7109
|
const OptionChainStyles = `
|
|
6878
|
-
${
|
|
7110
|
+
${BaseStyles}
|
|
7111
|
+
${getLoadingOverlayStyles('option-chain-widget')}
|
|
7112
|
+
${getErrorStyles('option-chain-widget')}
|
|
7113
|
+
${getFooterStyles('option-chain-widget')}
|
|
7114
|
+
${getNoDataStyles('option-chain-widget')}
|
|
6879
7115
|
|
|
6880
|
-
/*
|
|
7116
|
+
/* Option Chain Widget Specific Styles */
|
|
6881
7117
|
.option-chain-widget {
|
|
6882
7118
|
border: 1px solid #e5e7eb;
|
|
6883
7119
|
border-radius: 8px;
|
|
@@ -6902,8 +7138,22 @@ ${SharedStyles}
|
|
|
6902
7138
|
flex-wrap: wrap;
|
|
6903
7139
|
}
|
|
6904
7140
|
|
|
7141
|
+
.option-chain-widget .filter-section {
|
|
7142
|
+
display: flex;
|
|
7143
|
+
gap: 8px;
|
|
7144
|
+
align-items: center;
|
|
7145
|
+
margin-top: 8px;
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7148
|
+
.option-chain-widget .filter-label {
|
|
7149
|
+
font-size: 13px;
|
|
7150
|
+
font-weight: 500;
|
|
7151
|
+
color: #374151;
|
|
7152
|
+
}
|
|
7153
|
+
|
|
6905
7154
|
.option-chain-widget .symbol-input,
|
|
6906
|
-
.option-chain-widget .date-select
|
|
7155
|
+
.option-chain-widget .date-select,
|
|
7156
|
+
.option-chain-widget .strike-filter {
|
|
6907
7157
|
padding: 8px 12px;
|
|
6908
7158
|
border: 1px solid #d1d5db;
|
|
6909
7159
|
border-radius: 4px;
|
|
@@ -6924,6 +7174,12 @@ ${SharedStyles}
|
|
|
6924
7174
|
background: white;
|
|
6925
7175
|
}
|
|
6926
7176
|
|
|
7177
|
+
.option-chain-widget .strike-filter {
|
|
7178
|
+
min-width: 200px;
|
|
7179
|
+
background: white;
|
|
7180
|
+
cursor: pointer;
|
|
7181
|
+
}
|
|
7182
|
+
|
|
6927
7183
|
.option-chain-widget .fetch-button {
|
|
6928
7184
|
padding: 8px 16px;
|
|
6929
7185
|
background: #3b82f6;
|
|
@@ -7079,10 +7335,22 @@ ${SharedStyles}
|
|
|
7079
7335
|
min-height: 32px;
|
|
7080
7336
|
}
|
|
7081
7337
|
|
|
7338
|
+
/* In-the-money highlighting */
|
|
7339
|
+
.option-chain-widget .calls-data.in-the-money span,
|
|
7340
|
+
.option-chain-widget .puts-data.in-the-money span {
|
|
7341
|
+
background-color: #fffbeb;
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7082
7344
|
.option-chain-widget .option-row:hover {
|
|
7083
7345
|
background: #f9fafb;
|
|
7084
7346
|
}
|
|
7085
|
-
|
|
7347
|
+
|
|
7348
|
+
.option-chain-widget .option-row:hover .calls-data.in-the-money span,
|
|
7349
|
+
.option-chain-widget .option-row:hover .puts-data.in-the-money span {
|
|
7350
|
+
background-color: #fef3c7;
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
.option-chain-widget .option-row:hover .calls-data span,
|
|
7086
7354
|
.option-chain-widget .option-row:hover .puts-data span,
|
|
7087
7355
|
.option-chain-widget .option-row:hover .strike-data {
|
|
7088
7356
|
background: #f9fafb;
|
|
@@ -7106,14 +7374,32 @@ ${SharedStyles}
|
|
|
7106
7374
|
.option-chain-widget .puts-data span {
|
|
7107
7375
|
padding: 6px 4px;
|
|
7108
7376
|
text-align: center;
|
|
7109
|
-
font-size: 12px;
|
|
7110
|
-
font-weight: 400;
|
|
7111
|
-
color: #111827;
|
|
7377
|
+
font-size: 12px;
|
|
7378
|
+
font-weight: 400;
|
|
7379
|
+
color: #111827;
|
|
7112
7380
|
overflow: hidden;
|
|
7113
7381
|
text-overflow: ellipsis;
|
|
7114
7382
|
white-space: nowrap;
|
|
7115
7383
|
}
|
|
7116
7384
|
|
|
7385
|
+
/* Override color for change values with positive/negative classes */
|
|
7386
|
+
.option-chain-widget .calls-data span.positive,
|
|
7387
|
+
.option-chain-widget .puts-data span.positive {
|
|
7388
|
+
color: #059669 !important;
|
|
7389
|
+
font-weight: 500;
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
.option-chain-widget .calls-data span.negative,
|
|
7393
|
+
.option-chain-widget .puts-data span.negative {
|
|
7394
|
+
color: #dc2626 !important;
|
|
7395
|
+
font-weight: 500;
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7398
|
+
.option-chain-widget .calls-data span.neutral,
|
|
7399
|
+
.option-chain-widget .puts-data span.neutral {
|
|
7400
|
+
color: #6b7280 !important;
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7117
7403
|
.option-chain-widget .contract-cell {
|
|
7118
7404
|
font-size: 10px;
|
|
7119
7405
|
color: #6b7280;
|
|
@@ -7125,78 +7411,42 @@ ${SharedStyles}
|
|
|
7125
7411
|
white-space: nowrap;
|
|
7126
7412
|
}
|
|
7127
7413
|
|
|
7128
|
-
.option-chain-widget .
|
|
7129
|
-
color: #
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
color: #dc2626;
|
|
7134
|
-
}
|
|
7135
|
-
|
|
7136
|
-
.option-chain-widget .neutral {
|
|
7137
|
-
color: #6b7280;
|
|
7138
|
-
}
|
|
7139
|
-
|
|
7140
|
-
.option-chain-widget .widget-footer {
|
|
7141
|
-
background: #f9fafb;
|
|
7142
|
-
padding: 8px 16px;
|
|
7143
|
-
border-top: 1px solid #e5e7eb;
|
|
7144
|
-
text-align: center;
|
|
7145
|
-
font-size: 11px;
|
|
7146
|
-
color: #6b7280;
|
|
7414
|
+
.option-chain-widget .contract-cell.clickable {
|
|
7415
|
+
color: #3b82f6;
|
|
7416
|
+
cursor: pointer;
|
|
7417
|
+
text-decoration: underline;
|
|
7418
|
+
transition: all 0.2s ease;
|
|
7147
7419
|
}
|
|
7148
7420
|
|
|
7149
|
-
.option-chain-widget .
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
left: 0;
|
|
7153
|
-
right: 0;
|
|
7154
|
-
bottom: 0;
|
|
7155
|
-
background: rgba(255, 255, 255, 0.9);
|
|
7156
|
-
display: flex;
|
|
7157
|
-
align-items: center;
|
|
7158
|
-
justify-content: center;
|
|
7159
|
-
z-index: 20;
|
|
7421
|
+
.option-chain-widget .contract-cell.clickable:hover {
|
|
7422
|
+
color: #2563eb;
|
|
7423
|
+
background-color: #eff6ff;
|
|
7160
7424
|
}
|
|
7161
7425
|
|
|
7162
|
-
.option-chain-widget .
|
|
7163
|
-
|
|
7426
|
+
.option-chain-widget .contract-cell.clickable:focus {
|
|
7427
|
+
outline: 2px solid #3b82f6;
|
|
7428
|
+
outline-offset: 2px;
|
|
7429
|
+
border-radius: 2px;
|
|
7164
7430
|
}
|
|
7165
7431
|
|
|
7166
|
-
.option-chain-widget .
|
|
7167
|
-
|
|
7432
|
+
.option-chain-widget .contract-cell.clickable:active {
|
|
7433
|
+
color: #1e40af;
|
|
7434
|
+
background-color: #dbeafe;
|
|
7168
7435
|
}
|
|
7169
7436
|
|
|
7170
|
-
.option-chain-widget .
|
|
7171
|
-
|
|
7172
|
-
height: 32px;
|
|
7173
|
-
border: 3px solid #e5e7eb;
|
|
7174
|
-
border-top: 3px solid #3b82f6;
|
|
7175
|
-
border-radius: 50%;
|
|
7176
|
-
animation: spin 1s linear infinite;
|
|
7177
|
-
margin: 0 auto 12px;
|
|
7437
|
+
.option-chain-widget .positive {
|
|
7438
|
+
color: #059669;
|
|
7178
7439
|
}
|
|
7179
7440
|
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
100% { transform: rotate(360deg); }
|
|
7441
|
+
.option-chain-widget .negative {
|
|
7442
|
+
color: #dc2626;
|
|
7183
7443
|
}
|
|
7184
7444
|
|
|
7185
|
-
.option-chain-widget .
|
|
7186
|
-
padding: 40px;
|
|
7187
|
-
text-align: center;
|
|
7445
|
+
.option-chain-widget .neutral {
|
|
7188
7446
|
color: #6b7280;
|
|
7189
7447
|
}
|
|
7190
7448
|
|
|
7191
|
-
|
|
7192
|
-
padding: 20px;
|
|
7193
|
-
background: #fef2f2;
|
|
7194
|
-
color: #dc2626;
|
|
7195
|
-
border: 1px solid #fecaca;
|
|
7196
|
-
margin: 16px;
|
|
7197
|
-
border-radius: 4px;
|
|
7198
|
-
text-align: center;
|
|
7199
|
-
}
|
|
7449
|
+
/* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
|
|
7200
7450
|
|
|
7201
7451
|
/* RESPONSIVE STYLES */
|
|
7202
7452
|
|
|
@@ -7207,13 +7457,19 @@ ${SharedStyles}
|
|
|
7207
7457
|
align-items: stretch;
|
|
7208
7458
|
gap: 8px;
|
|
7209
7459
|
}
|
|
7210
|
-
|
|
7460
|
+
|
|
7461
|
+
.option-chain-widget .filter-section {
|
|
7462
|
+
flex-direction: row;
|
|
7463
|
+
margin-top: 0;
|
|
7464
|
+
}
|
|
7465
|
+
|
|
7211
7466
|
.option-chain-widget .symbol-input,
|
|
7212
|
-
.option-chain-widget .date-select
|
|
7467
|
+
.option-chain-widget .date-select,
|
|
7468
|
+
.option-chain-widget .strike-filter {
|
|
7213
7469
|
width: 100%;
|
|
7214
7470
|
max-width: none;
|
|
7215
7471
|
}
|
|
7216
|
-
|
|
7472
|
+
|
|
7217
7473
|
.option-chain-widget .fetch-button {
|
|
7218
7474
|
width: 100%;
|
|
7219
7475
|
}
|
|
@@ -7369,6 +7625,121 @@ ${SharedStyles}
|
|
|
7369
7625
|
padding: 2px 1px;
|
|
7370
7626
|
}
|
|
7371
7627
|
}
|
|
7628
|
+
|
|
7629
|
+
/* Modal Styles */
|
|
7630
|
+
.option-chain-modal-overlay {
|
|
7631
|
+
position: fixed;
|
|
7632
|
+
top: 0;
|
|
7633
|
+
left: 0;
|
|
7634
|
+
right: 0;
|
|
7635
|
+
bottom: 0;
|
|
7636
|
+
background: rgba(0, 0, 0, 0.6);
|
|
7637
|
+
display: flex;
|
|
7638
|
+
align-items: center;
|
|
7639
|
+
justify-content: center;
|
|
7640
|
+
z-index: 10000;
|
|
7641
|
+
padding: 20px;
|
|
7642
|
+
animation: fadeIn 0.2s ease-out;
|
|
7643
|
+
}
|
|
7644
|
+
|
|
7645
|
+
@keyframes fadeIn {
|
|
7646
|
+
from {
|
|
7647
|
+
opacity: 0;
|
|
7648
|
+
}
|
|
7649
|
+
to {
|
|
7650
|
+
opacity: 1;
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7654
|
+
.option-chain-modal {
|
|
7655
|
+
background: white;
|
|
7656
|
+
border-radius: 12px;
|
|
7657
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
7658
|
+
max-width: 800px;
|
|
7659
|
+
width: 100%;
|
|
7660
|
+
max-height: 90vh;
|
|
7661
|
+
display: flex;
|
|
7662
|
+
flex-direction: column;
|
|
7663
|
+
animation: slideUp 0.3s ease-out;
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
@keyframes slideUp {
|
|
7667
|
+
from {
|
|
7668
|
+
transform: translateY(20px);
|
|
7669
|
+
opacity: 0;
|
|
7670
|
+
}
|
|
7671
|
+
to {
|
|
7672
|
+
transform: translateY(0);
|
|
7673
|
+
opacity: 1;
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
.option-chain-modal-header {
|
|
7678
|
+
display: flex;
|
|
7679
|
+
align-items: center;
|
|
7680
|
+
justify-content: space-between;
|
|
7681
|
+
padding: 20px 24px;
|
|
7682
|
+
border-bottom: 1px solid #e5e7eb;
|
|
7683
|
+
}
|
|
7684
|
+
|
|
7685
|
+
.option-chain-modal-header h3 {
|
|
7686
|
+
margin: 0;
|
|
7687
|
+
font-size: 18px;
|
|
7688
|
+
font-weight: 600;
|
|
7689
|
+
color: #111827;
|
|
7690
|
+
}
|
|
7691
|
+
|
|
7692
|
+
.option-chain-modal-close {
|
|
7693
|
+
background: none;
|
|
7694
|
+
border: none;
|
|
7695
|
+
font-size: 28px;
|
|
7696
|
+
line-height: 1;
|
|
7697
|
+
color: #6b7280;
|
|
7698
|
+
cursor: pointer;
|
|
7699
|
+
padding: 0;
|
|
7700
|
+
width: 32px;
|
|
7701
|
+
height: 32px;
|
|
7702
|
+
display: flex;
|
|
7703
|
+
align-items: center;
|
|
7704
|
+
justify-content: center;
|
|
7705
|
+
border-radius: 4px;
|
|
7706
|
+
transition: all 0.2s ease;
|
|
7707
|
+
}
|
|
7708
|
+
|
|
7709
|
+
.option-chain-modal-close:hover {
|
|
7710
|
+
background: #f3f4f6;
|
|
7711
|
+
color: #111827;
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
.option-chain-modal-close:active {
|
|
7715
|
+
background: #e5e7eb;
|
|
7716
|
+
}
|
|
7717
|
+
|
|
7718
|
+
.option-chain-modal-body {
|
|
7719
|
+
flex: 1;
|
|
7720
|
+
overflow-y: auto;
|
|
7721
|
+
padding: 24px;
|
|
7722
|
+
}
|
|
7723
|
+
|
|
7724
|
+
/* Responsive modal */
|
|
7725
|
+
@media (max-width: 768px) {
|
|
7726
|
+
.option-chain-modal {
|
|
7727
|
+
max-width: 95%;
|
|
7728
|
+
max-height: 95vh;
|
|
7729
|
+
}
|
|
7730
|
+
|
|
7731
|
+
.option-chain-modal-header {
|
|
7732
|
+
padding: 16px;
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7735
|
+
.option-chain-modal-header h3 {
|
|
7736
|
+
font-size: 16px;
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
.option-chain-modal-body {
|
|
7740
|
+
padding: 16px;
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7372
7743
|
`;
|
|
7373
7744
|
|
|
7374
7745
|
class OptionChainWidget extends BaseWidget {
|
|
@@ -7383,7 +7754,18 @@ ${SharedStyles}
|
|
|
7383
7754
|
this.data = null;
|
|
7384
7755
|
this.isDestroyed = false;
|
|
7385
7756
|
this.unsubscribe = null;
|
|
7757
|
+
this.unsubscribeUnderlying = null; // For underlying stock price
|
|
7758
|
+
this.underlyingPrice = null; // Current price of underlying stock
|
|
7386
7759
|
this.loadingTimeout = null;
|
|
7760
|
+
this.renderDebounceTimeout = null; // For debouncing re-renders
|
|
7761
|
+
|
|
7762
|
+
// Separate cache for option chain data and underlying price
|
|
7763
|
+
this.cachedOptionChainData = null;
|
|
7764
|
+
this.cachedUnderlyingPrice = null;
|
|
7765
|
+
|
|
7766
|
+
// Modal for Options widget
|
|
7767
|
+
this.optionsModal = null;
|
|
7768
|
+
this.optionsWidgetInstance = null;
|
|
7387
7769
|
|
|
7388
7770
|
// INPUT VALIDATION: Validate initial symbol if provided
|
|
7389
7771
|
if (options.symbol) {
|
|
@@ -7399,6 +7781,7 @@ ${SharedStyles}
|
|
|
7399
7781
|
}
|
|
7400
7782
|
this.date = '';
|
|
7401
7783
|
this.availableDates = {};
|
|
7784
|
+
this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
|
|
7402
7785
|
|
|
7403
7786
|
// RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
|
|
7404
7787
|
this.fetchRateLimiter = createRateLimitValidator(1000);
|
|
@@ -7429,6 +7812,7 @@ ${SharedStyles}
|
|
|
7429
7812
|
this.dateSelect = this.container.querySelector('.date-select');
|
|
7430
7813
|
this.fetchButton = this.container.querySelector('.fetch-button');
|
|
7431
7814
|
this.dataGrid = this.container.querySelector('.option-chain-data-grid');
|
|
7815
|
+
this.strikeFilterSelect = this.container.querySelector('.strike-filter');
|
|
7432
7816
|
|
|
7433
7817
|
// Set initial symbol value if provided in options
|
|
7434
7818
|
if (this.symbol) {
|
|
@@ -7503,6 +7887,22 @@ ${SharedStyles}
|
|
|
7503
7887
|
}
|
|
7504
7888
|
this.loadAvailableDates(symbolValidation.sanitized);
|
|
7505
7889
|
});
|
|
7890
|
+
|
|
7891
|
+
// MEMORY LEAK FIX: Use BaseWidget's addEventListener
|
|
7892
|
+
// Strike filter dropdown
|
|
7893
|
+
this.addEventListener(this.strikeFilterSelect, 'change', e => {
|
|
7894
|
+
const value = e.target.value;
|
|
7895
|
+
if (value === 'all') {
|
|
7896
|
+
this.strikeFilterRange = 'all';
|
|
7897
|
+
} else {
|
|
7898
|
+
this.strikeFilterRange = parseInt(value, 10);
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7901
|
+
// Re-render with new filter if we have data
|
|
7902
|
+
if (this.data) {
|
|
7903
|
+
this.displayOptionChain(this.data);
|
|
7904
|
+
}
|
|
7905
|
+
});
|
|
7506
7906
|
}
|
|
7507
7907
|
async loadAvailableDates(symbol) {
|
|
7508
7908
|
if (!symbol) return;
|
|
@@ -7518,7 +7918,9 @@ ${SharedStyles}
|
|
|
7518
7918
|
// Use API service from wsManager
|
|
7519
7919
|
const apiService = this.wsManager.getApiService();
|
|
7520
7920
|
const data = await apiService.getOptionChainDates(symbol);
|
|
7521
|
-
|
|
7921
|
+
if (this.debug) {
|
|
7922
|
+
console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
|
|
7923
|
+
}
|
|
7522
7924
|
this.availableDates = data.dates_dictionary || {};
|
|
7523
7925
|
this.populateDateOptions();
|
|
7524
7926
|
|
|
@@ -7612,11 +8014,15 @@ ${SharedStyles}
|
|
|
7612
8014
|
this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
|
|
7613
8015
|
}, 10000); // 10 second timeout
|
|
7614
8016
|
|
|
7615
|
-
// Unsubscribe from previous
|
|
8017
|
+
// Unsubscribe from previous subscriptions if they exist
|
|
7616
8018
|
if (this.unsubscribe) {
|
|
7617
8019
|
this.unsubscribe();
|
|
7618
8020
|
this.unsubscribe = null;
|
|
7619
8021
|
}
|
|
8022
|
+
if (this.unsubscribeUnderlying) {
|
|
8023
|
+
this.unsubscribeUnderlying();
|
|
8024
|
+
this.unsubscribeUnderlying = null;
|
|
8025
|
+
}
|
|
7620
8026
|
|
|
7621
8027
|
// Subscribe to option chain data
|
|
7622
8028
|
this.subscribeToData();
|
|
@@ -7626,25 +8032,62 @@ ${SharedStyles}
|
|
|
7626
8032
|
}
|
|
7627
8033
|
}
|
|
7628
8034
|
subscribeToData() {
|
|
7629
|
-
// Subscribe
|
|
7630
|
-
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
|
|
7631
|
-
// Pass symbol as string
|
|
7632
|
-
{
|
|
8035
|
+
// Subscribe to option chain data
|
|
8036
|
+
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
|
|
7633
8037
|
date: this.date
|
|
7634
|
-
}
|
|
7635
|
-
);
|
|
8038
|
+
});
|
|
7636
8039
|
|
|
7637
|
-
//
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
8040
|
+
// Subscribe to underlying stock price for ITM highlighting
|
|
8041
|
+
this.unsubscribeUnderlying = this.wsManager.subscribe(`${this.widgetId}-underlying`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
/**
|
|
8045
|
+
* Find the nearest strike price to the underlying price (ATM strike)
|
|
8046
|
+
* @param {Array} sortedStrikes - Array of strike prices sorted ascending
|
|
8047
|
+
* @param {Number} underlyingPrice - Current price of underlying stock
|
|
8048
|
+
* @returns {String} - The nearest strike price
|
|
8049
|
+
*/
|
|
8050
|
+
findNearestStrike(sortedStrikes, underlyingPrice) {
|
|
8051
|
+
if (!sortedStrikes || sortedStrikes.length === 0 || !underlyingPrice) {
|
|
8052
|
+
return null;
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
// Find strike closest to underlying price
|
|
8056
|
+
let nearestStrike = sortedStrikes[0];
|
|
8057
|
+
let minDifference = Math.abs(parseFloat(sortedStrikes[0]) - underlyingPrice);
|
|
8058
|
+
for (const strike of sortedStrikes) {
|
|
8059
|
+
const difference = Math.abs(parseFloat(strike) - underlyingPrice);
|
|
8060
|
+
if (difference < minDifference) {
|
|
8061
|
+
minDifference = difference;
|
|
8062
|
+
nearestStrike = strike;
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
return nearestStrike;
|
|
8066
|
+
}
|
|
8067
|
+
|
|
8068
|
+
/**
|
|
8069
|
+
* Filter strikes to show only those near the money
|
|
8070
|
+
* @param {Array} sortedStrikes - Array of strike prices sorted ascending
|
|
8071
|
+
* @param {String} atmStrike - The at-the-money strike price
|
|
8072
|
+
* @param {Number} range - Number of strikes to show above and below ATM
|
|
8073
|
+
* @returns {Array} - Filtered array of strikes
|
|
8074
|
+
*/
|
|
8075
|
+
filterNearMoneyStrikes(sortedStrikes, atmStrike, range) {
|
|
8076
|
+
if (!atmStrike || !sortedStrikes || sortedStrikes.length === 0) {
|
|
8077
|
+
return sortedStrikes;
|
|
8078
|
+
}
|
|
8079
|
+
const atmIndex = sortedStrikes.indexOf(atmStrike);
|
|
8080
|
+
if (atmIndex === -1) {
|
|
8081
|
+
return sortedStrikes;
|
|
8082
|
+
}
|
|
8083
|
+
const startIndex = Math.max(0, atmIndex - range);
|
|
8084
|
+
const endIndex = Math.min(sortedStrikes.length - 1, atmIndex + range);
|
|
8085
|
+
return sortedStrikes.slice(startIndex, endIndex + 1);
|
|
7643
8086
|
}
|
|
7644
8087
|
handleData(message) {
|
|
7645
|
-
// Clear loading timeout since we received data
|
|
8088
|
+
// Clear loading timeout since we received data (use BaseWidget method)
|
|
7646
8089
|
if (this.loadingTimeout) {
|
|
7647
|
-
clearTimeout(this.loadingTimeout);
|
|
8090
|
+
this.clearTimeout(this.loadingTimeout);
|
|
7648
8091
|
this.loadingTimeout = null;
|
|
7649
8092
|
}
|
|
7650
8093
|
|
|
@@ -7676,7 +8119,9 @@ ${SharedStyles}
|
|
|
7676
8119
|
}
|
|
7677
8120
|
}
|
|
7678
8121
|
} else {
|
|
7679
|
-
|
|
8122
|
+
// Cache the option chain data
|
|
8123
|
+
this.data = message;
|
|
8124
|
+
this.cachedOptionChainData = message;
|
|
7680
8125
|
this.displayOptionChain(message);
|
|
7681
8126
|
}
|
|
7682
8127
|
} else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
|
|
@@ -7691,9 +8136,35 @@ ${SharedStyles}
|
|
|
7691
8136
|
}
|
|
7692
8137
|
}
|
|
7693
8138
|
} else {
|
|
7694
|
-
|
|
8139
|
+
// Cache the option chain data
|
|
8140
|
+
this.data = message.data;
|
|
8141
|
+
this.cachedOptionChainData = message.data;
|
|
7695
8142
|
this.displayOptionChain(message.data);
|
|
7696
8143
|
}
|
|
8144
|
+
} else if (message.type === 'queryl1' && message.Data) {
|
|
8145
|
+
if (this.debug) {
|
|
8146
|
+
console.log('[OptionChainWidget] Received underlying price update');
|
|
8147
|
+
}
|
|
8148
|
+
const data = message.Data.find(d => d.Symbol === this.symbol);
|
|
8149
|
+
if (data && data.LastPx) {
|
|
8150
|
+
// Cache the underlying price
|
|
8151
|
+
this.underlyingPrice = parseFloat(data.LastPx);
|
|
8152
|
+
this.cachedUnderlyingPrice = parseFloat(data.LastPx);
|
|
8153
|
+
|
|
8154
|
+
// Debounce re-render to prevent excessive updates
|
|
8155
|
+
// Clear any pending render
|
|
8156
|
+
if (this.renderDebounceTimeout) {
|
|
8157
|
+
this.clearTimeout(this.renderDebounceTimeout);
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
// Schedule a debounced re-render (500ms delay)
|
|
8161
|
+
this.renderDebounceTimeout = this.setTimeout(() => {
|
|
8162
|
+
if (this.data) {
|
|
8163
|
+
this.displayOptionChain(this.data);
|
|
8164
|
+
}
|
|
8165
|
+
this.renderDebounceTimeout = null;
|
|
8166
|
+
}, 500);
|
|
8167
|
+
}
|
|
7697
8168
|
}
|
|
7698
8169
|
if (message._cached) {
|
|
7699
8170
|
this.showConnectionQuality(); // Show cache indicator
|
|
@@ -7701,6 +8172,14 @@ ${SharedStyles}
|
|
|
7701
8172
|
}
|
|
7702
8173
|
displayOptionChain(data) {
|
|
7703
8174
|
if (this.isDestroyed) return;
|
|
8175
|
+
|
|
8176
|
+
// Validate data
|
|
8177
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
8178
|
+
if (this.debug) {
|
|
8179
|
+
console.warn('[OptionChainWidget] Invalid or empty data passed to displayOptionChain');
|
|
8180
|
+
}
|
|
8181
|
+
return;
|
|
8182
|
+
}
|
|
7704
8183
|
try {
|
|
7705
8184
|
// Hide loading overlay
|
|
7706
8185
|
this.hideLoading();
|
|
@@ -7733,9 +8212,18 @@ ${SharedStyles}
|
|
|
7733
8212
|
}
|
|
7734
8213
|
});
|
|
7735
8214
|
|
|
7736
|
-
// Sort strikes
|
|
8215
|
+
// Sort strikes
|
|
7737
8216
|
const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
7738
|
-
|
|
8217
|
+
|
|
8218
|
+
// Apply ATM filter if not showing all
|
|
8219
|
+
let displayStrikes = sortedStrikes;
|
|
8220
|
+
if (this.strikeFilterRange !== 'all' && this.underlyingPrice) {
|
|
8221
|
+
const atmStrike = this.findNearestStrike(sortedStrikes, this.underlyingPrice);
|
|
8222
|
+
if (atmStrike) {
|
|
8223
|
+
displayStrikes = this.filterNearMoneyStrikes(sortedStrikes, atmStrike, this.strikeFilterRange);
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
displayStrikes.forEach(strike => {
|
|
7739
8227
|
const {
|
|
7740
8228
|
call,
|
|
7741
8229
|
put
|
|
@@ -7754,7 +8242,7 @@ ${SharedStyles}
|
|
|
7754
8242
|
const formatted = parseFloat(change).toFixed(2);
|
|
7755
8243
|
const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
|
|
7756
8244
|
const sign = change > 0 ? '+' : '';
|
|
7757
|
-
span.className =
|
|
8245
|
+
span.className = className;
|
|
7758
8246
|
span.textContent = `${sign}${formatted}`;
|
|
7759
8247
|
return span;
|
|
7760
8248
|
};
|
|
@@ -7767,7 +8255,28 @@ ${SharedStyles}
|
|
|
7767
8255
|
// Create calls data section
|
|
7768
8256
|
const callsData = document.createElement('div');
|
|
7769
8257
|
callsData.className = 'calls-data';
|
|
7770
|
-
|
|
8258
|
+
|
|
8259
|
+
// Add ITM highlighting for calls (ITM when strike < underlying price)
|
|
8260
|
+
if (this.underlyingPrice && parseFloat(strike) < this.underlyingPrice) {
|
|
8261
|
+
callsData.classList.add('in-the-money');
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
// Make contract cell clickable
|
|
8265
|
+
const callContractCell = createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell');
|
|
8266
|
+
if (call && call.symbol) {
|
|
8267
|
+
callContractCell.classList.add('clickable');
|
|
8268
|
+
callContractCell.setAttribute('role', 'button');
|
|
8269
|
+
callContractCell.setAttribute('tabindex', '0');
|
|
8270
|
+
callContractCell.setAttribute('data-symbol', call.symbol);
|
|
8271
|
+
this.addEventListener(callContractCell, 'click', () => this.showOptionsModal(call.symbol));
|
|
8272
|
+
this.addEventListener(callContractCell, 'keypress', e => {
|
|
8273
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
8274
|
+
e.preventDefault();
|
|
8275
|
+
this.showOptionsModal(call.symbol);
|
|
8276
|
+
}
|
|
8277
|
+
});
|
|
8278
|
+
}
|
|
8279
|
+
callsData.appendChild(callContractCell);
|
|
7771
8280
|
callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
|
|
7772
8281
|
const callChangeSpan = document.createElement('span');
|
|
7773
8282
|
if (call) {
|
|
@@ -7790,7 +8299,28 @@ ${SharedStyles}
|
|
|
7790
8299
|
// Create puts data section
|
|
7791
8300
|
const putsData = document.createElement('div');
|
|
7792
8301
|
putsData.className = 'puts-data';
|
|
7793
|
-
|
|
8302
|
+
|
|
8303
|
+
// Add ITM highlighting for puts (ITM when strike > underlying price)
|
|
8304
|
+
if (this.underlyingPrice && parseFloat(strike) > this.underlyingPrice) {
|
|
8305
|
+
putsData.classList.add('in-the-money');
|
|
8306
|
+
}
|
|
8307
|
+
|
|
8308
|
+
// Make contract cell clickable
|
|
8309
|
+
const putContractCell = createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell');
|
|
8310
|
+
if (put && put.symbol) {
|
|
8311
|
+
putContractCell.classList.add('clickable');
|
|
8312
|
+
putContractCell.setAttribute('role', 'button');
|
|
8313
|
+
putContractCell.setAttribute('tabindex', '0');
|
|
8314
|
+
putContractCell.setAttribute('data-symbol', put.symbol);
|
|
8315
|
+
this.addEventListener(putContractCell, 'click', () => this.showOptionsModal(put.symbol));
|
|
8316
|
+
this.addEventListener(putContractCell, 'keypress', e => {
|
|
8317
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
8318
|
+
e.preventDefault();
|
|
8319
|
+
this.showOptionsModal(put.symbol);
|
|
8320
|
+
}
|
|
8321
|
+
});
|
|
8322
|
+
}
|
|
8323
|
+
putsData.appendChild(putContractCell);
|
|
7794
8324
|
putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
|
|
7795
8325
|
const putChangeSpan = document.createElement('span');
|
|
7796
8326
|
if (put) {
|
|
@@ -7814,14 +8344,22 @@ ${SharedStyles}
|
|
|
7814
8344
|
|
|
7815
8345
|
// Add footer - with array length check
|
|
7816
8346
|
if (data && data.length > 0) {
|
|
7817
|
-
const
|
|
8347
|
+
const isDelayed = data[0].DataSource === 'OPRA-D';
|
|
8348
|
+
|
|
8349
|
+
// Get timestamp - subtract 20 minutes if delayed data
|
|
8350
|
+
let timestampValue = data[0].quoteTime || Date.now();
|
|
8351
|
+
if (isDelayed) {
|
|
8352
|
+
// Subtract 20 minutes (20 * 60 * 1000 milliseconds)
|
|
8353
|
+
timestampValue = timestampValue - 20 * 60 * 1000;
|
|
8354
|
+
}
|
|
8355
|
+
const timestamp = formatTimestampET(timestampValue);
|
|
7818
8356
|
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
7819
8357
|
if (lastUpdateElement) {
|
|
7820
8358
|
lastUpdateElement.textContent = `Last update: ${timestamp}`;
|
|
7821
8359
|
}
|
|
7822
8360
|
|
|
7823
8361
|
// Update footer
|
|
7824
|
-
const source =
|
|
8362
|
+
const source = isDelayed ? '20 mins delayed' : 'Real-time';
|
|
7825
8363
|
const dataSourceElement = this.container.querySelector('.data-source');
|
|
7826
8364
|
if (dataSourceElement) {
|
|
7827
8365
|
dataSourceElement.textContent = `Source: ${source}`;
|
|
@@ -7844,35 +8382,6 @@ ${SharedStyles}
|
|
|
7844
8382
|
loadingOverlay.classList.add('hidden');
|
|
7845
8383
|
}
|
|
7846
8384
|
}
|
|
7847
|
-
|
|
7848
|
-
/* showInputError(message) {
|
|
7849
|
-
const errorDiv = document.createElement('div');
|
|
7850
|
-
errorDiv.className = 'symbol-error-message';
|
|
7851
|
-
errorDiv.textContent = message;
|
|
7852
|
-
errorDiv.style.cssText = `
|
|
7853
|
-
color: #dc2626;
|
|
7854
|
-
font-size: 12px;
|
|
7855
|
-
margin-bottom: 2px;
|
|
7856
|
-
position: absolute;
|
|
7857
|
-
background: white;
|
|
7858
|
-
z-index: 1000;
|
|
7859
|
-
transform: translateY(-100%);
|
|
7860
|
-
top: -4px;
|
|
7861
|
-
left: 0; // Align with input box
|
|
7862
|
-
`;
|
|
7863
|
-
|
|
7864
|
-
// Ensure the parent is positioned
|
|
7865
|
-
const parent = this.symbolInput;
|
|
7866
|
-
if (parent) {
|
|
7867
|
-
parent.style.position = 'relative';
|
|
7868
|
-
// Insert the errorDiv as the first child of the parent
|
|
7869
|
-
parent.insertBefore(errorDiv, parent.firstChild);
|
|
7870
|
-
}
|
|
7871
|
-
|
|
7872
|
-
this.symbolInput.errorMessage = errorDiv;
|
|
7873
|
-
this.symbolInput.focus();
|
|
7874
|
-
} */
|
|
7875
|
-
|
|
7876
8385
|
showError(message) {
|
|
7877
8386
|
this.hideLoading();
|
|
7878
8387
|
this.clearError();
|
|
@@ -7945,19 +8454,135 @@ ${SharedStyles}
|
|
|
7945
8454
|
this.container.appendChild(noDataElement);
|
|
7946
8455
|
}
|
|
7947
8456
|
}
|
|
8457
|
+
|
|
8458
|
+
/**
|
|
8459
|
+
* Show Options widget in a modal for a specific option contract
|
|
8460
|
+
* @param {string} optionSymbol - The option contract symbol
|
|
8461
|
+
*/
|
|
8462
|
+
showOptionsModal(optionSymbol) {
|
|
8463
|
+
if (!optionSymbol || optionSymbol === '--') {
|
|
8464
|
+
return;
|
|
8465
|
+
}
|
|
8466
|
+
|
|
8467
|
+
// Close existing modal if any
|
|
8468
|
+
this.closeOptionsModal();
|
|
8469
|
+
|
|
8470
|
+
// Create modal overlay
|
|
8471
|
+
this.optionsModal = document.createElement('div');
|
|
8472
|
+
this.optionsModal.className = 'option-chain-modal-overlay';
|
|
8473
|
+
this.optionsModal.innerHTML = `
|
|
8474
|
+
<div class="option-chain-modal">
|
|
8475
|
+
<div class="option-chain-modal-header">
|
|
8476
|
+
<h3>Option Details: ${sanitizeSymbol(optionSymbol)}</h3>
|
|
8477
|
+
<button class="option-chain-modal-close" aria-label="Close modal">×</button>
|
|
8478
|
+
</div>
|
|
8479
|
+
<div class="option-chain-modal-body">
|
|
8480
|
+
<div id="option-chain-modal-widget"></div>
|
|
8481
|
+
</div>
|
|
8482
|
+
</div>
|
|
8483
|
+
`;
|
|
8484
|
+
|
|
8485
|
+
// Add to body
|
|
8486
|
+
document.body.appendChild(this.optionsModal);
|
|
8487
|
+
|
|
8488
|
+
// Add close event
|
|
8489
|
+
const closeBtn = this.optionsModal.querySelector('.option-chain-modal-close');
|
|
8490
|
+
this.addEventListener(closeBtn, 'click', () => this.closeOptionsModal());
|
|
8491
|
+
|
|
8492
|
+
// Close on overlay click
|
|
8493
|
+
this.addEventListener(this.optionsModal, 'click', e => {
|
|
8494
|
+
if (e.target === this.optionsModal) {
|
|
8495
|
+
this.closeOptionsModal();
|
|
8496
|
+
}
|
|
8497
|
+
});
|
|
8498
|
+
|
|
8499
|
+
// Close on Escape key
|
|
8500
|
+
const escapeHandler = e => {
|
|
8501
|
+
if (e.key === 'Escape') {
|
|
8502
|
+
this.closeOptionsModal();
|
|
8503
|
+
}
|
|
8504
|
+
};
|
|
8505
|
+
document.addEventListener('keydown', escapeHandler);
|
|
8506
|
+
this._escapeHandler = escapeHandler;
|
|
8507
|
+
|
|
8508
|
+
// Create Options widget instance
|
|
8509
|
+
try {
|
|
8510
|
+
const widgetContainer = this.optionsModal.querySelector('#option-chain-modal-widget');
|
|
8511
|
+
this.optionsWidgetInstance = new OptionsWidget(widgetContainer, {
|
|
8512
|
+
symbol: optionSymbol,
|
|
8513
|
+
wsManager: this.wsManager,
|
|
8514
|
+
debug: this.debug,
|
|
8515
|
+
styled: true
|
|
8516
|
+
}, `${this.widgetId}-options-modal`);
|
|
8517
|
+
if (this.debug) {
|
|
8518
|
+
console.log('[OptionChainWidget] Options modal opened for:', optionSymbol);
|
|
8519
|
+
}
|
|
8520
|
+
} catch (error) {
|
|
8521
|
+
console.error('[OptionChainWidget] Error creating Options widget:', error);
|
|
8522
|
+
this.closeOptionsModal();
|
|
8523
|
+
this.showError(`Failed to load option details: ${error.message}`);
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
|
|
8527
|
+
/**
|
|
8528
|
+
* Close the Options modal and cleanup
|
|
8529
|
+
*/
|
|
8530
|
+
closeOptionsModal() {
|
|
8531
|
+
// Destroy widget instance
|
|
8532
|
+
if (this.optionsWidgetInstance) {
|
|
8533
|
+
this.optionsWidgetInstance.destroy();
|
|
8534
|
+
this.optionsWidgetInstance = null;
|
|
8535
|
+
}
|
|
8536
|
+
|
|
8537
|
+
// Remove modal from DOM
|
|
8538
|
+
if (this.optionsModal) {
|
|
8539
|
+
this.optionsModal.remove();
|
|
8540
|
+
this.optionsModal = null;
|
|
8541
|
+
}
|
|
8542
|
+
|
|
8543
|
+
// Remove escape key handler
|
|
8544
|
+
if (this._escapeHandler) {
|
|
8545
|
+
document.removeEventListener('keydown', this._escapeHandler);
|
|
8546
|
+
this._escapeHandler = null;
|
|
8547
|
+
}
|
|
8548
|
+
if (this.debug) {
|
|
8549
|
+
console.log('[OptionChainWidget] Options modal closed');
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
7948
8552
|
destroy() {
|
|
7949
8553
|
this.isDestroyed = true;
|
|
8554
|
+
|
|
8555
|
+
// Close modal if open
|
|
8556
|
+
this.closeOptionsModal();
|
|
7950
8557
|
if (this.unsubscribe) {
|
|
7951
8558
|
this.unsubscribe();
|
|
7952
8559
|
this.unsubscribe = null;
|
|
7953
8560
|
}
|
|
7954
8561
|
|
|
8562
|
+
// Unsubscribe from underlying price
|
|
8563
|
+
if (this.unsubscribeUnderlying) {
|
|
8564
|
+
this.unsubscribeUnderlying();
|
|
8565
|
+
this.unsubscribeUnderlying = null;
|
|
8566
|
+
}
|
|
8567
|
+
|
|
7955
8568
|
// MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
|
|
7956
8569
|
if (this.loadingTimeout) {
|
|
7957
8570
|
this.clearTimeout(this.loadingTimeout);
|
|
7958
8571
|
this.loadingTimeout = null;
|
|
7959
8572
|
}
|
|
7960
8573
|
|
|
8574
|
+
// Clear debounce timeout
|
|
8575
|
+
if (this.renderDebounceTimeout) {
|
|
8576
|
+
this.clearTimeout(this.renderDebounceTimeout);
|
|
8577
|
+
this.renderDebounceTimeout = null;
|
|
8578
|
+
}
|
|
8579
|
+
|
|
8580
|
+
// Clear cached data
|
|
8581
|
+
this.cachedOptionChainData = null;
|
|
8582
|
+
this.cachedUnderlyingPrice = null;
|
|
8583
|
+
this.underlyingPrice = null;
|
|
8584
|
+
this.data = null;
|
|
8585
|
+
|
|
7961
8586
|
// Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
|
|
7962
8587
|
super.destroy();
|
|
7963
8588
|
}
|
|
@@ -8043,80 +8668,81 @@ ${SharedStyles}
|
|
|
8043
8668
|
font-family: Arial, sans-serif;
|
|
8044
8669
|
}
|
|
8045
8670
|
|
|
8046
|
-
.widget-header {
|
|
8671
|
+
.data-widget .widget-header {
|
|
8047
8672
|
display: flex;
|
|
8048
8673
|
flex-direction: column;
|
|
8049
8674
|
margin-bottom: 10px;
|
|
8050
8675
|
}
|
|
8051
8676
|
|
|
8052
|
-
.symbol {
|
|
8677
|
+
.data-widget .symbol {
|
|
8053
8678
|
font-size: 24px;
|
|
8054
8679
|
font-weight: bold;
|
|
8055
8680
|
margin-right: 10px;
|
|
8056
8681
|
}
|
|
8057
8682
|
|
|
8058
|
-
.company-info {
|
|
8683
|
+
.data-widget .company-info {
|
|
8059
8684
|
font-size: 12px;
|
|
8060
8685
|
color: #666;
|
|
8061
8686
|
}
|
|
8062
8687
|
|
|
8063
|
-
.trading-info {
|
|
8688
|
+
.data-widget .trading-info {
|
|
8064
8689
|
font-size: 12px;
|
|
8065
8690
|
color: #999;
|
|
8066
8691
|
}
|
|
8067
8692
|
|
|
8068
|
-
.price-section {
|
|
8693
|
+
.data-widget .price-section {
|
|
8069
8694
|
display: flex;
|
|
8070
8695
|
align-items: baseline;
|
|
8071
8696
|
margin-bottom: 10px;
|
|
8072
8697
|
}
|
|
8073
8698
|
|
|
8074
|
-
.current-price {
|
|
8699
|
+
.data-widget .current-price {
|
|
8075
8700
|
font-size: 36px;
|
|
8076
8701
|
font-weight: bold;
|
|
8077
8702
|
margin-right: 10px;
|
|
8078
8703
|
}
|
|
8079
8704
|
|
|
8080
|
-
.price-change {
|
|
8705
|
+
.data-widget .price-change {
|
|
8081
8706
|
font-size: 18px;
|
|
8082
8707
|
}
|
|
8083
8708
|
|
|
8084
|
-
.price-change.positive {
|
|
8709
|
+
.data-widget .price-change.positive {
|
|
8085
8710
|
color: green;
|
|
8086
8711
|
}
|
|
8087
8712
|
|
|
8088
|
-
.price-change.negative {
|
|
8713
|
+
.data-widget .price-change.negative {
|
|
8089
8714
|
color: red;
|
|
8090
8715
|
}
|
|
8091
8716
|
|
|
8092
|
-
.bid-ask-section {
|
|
8717
|
+
.data-widget .bid-ask-section {
|
|
8093
8718
|
display: flex;
|
|
8094
8719
|
justify-content: space-between;
|
|
8095
8720
|
margin-bottom: 10px;
|
|
8096
8721
|
}
|
|
8097
8722
|
|
|
8098
|
-
.ask,
|
|
8723
|
+
.data-widget .ask,
|
|
8724
|
+
.data-widget .bid {
|
|
8099
8725
|
display: flex;
|
|
8100
8726
|
align-items: center;
|
|
8101
8727
|
}
|
|
8102
8728
|
|
|
8103
|
-
.label {
|
|
8729
|
+
.data-widget .label {
|
|
8104
8730
|
font-size: 14px;
|
|
8105
8731
|
margin-right: 5px;
|
|
8106
8732
|
}
|
|
8107
8733
|
|
|
8108
|
-
.value {
|
|
8734
|
+
.data-widget .value {
|
|
8109
8735
|
font-size: 16px;
|
|
8110
8736
|
font-weight: bold;
|
|
8111
8737
|
margin-right: 5px;
|
|
8112
8738
|
}
|
|
8113
8739
|
|
|
8114
|
-
.size {
|
|
8740
|
+
.data-widget .size {
|
|
8115
8741
|
font-size: 14px;
|
|
8116
8742
|
color: red;
|
|
8117
8743
|
}
|
|
8118
8744
|
|
|
8119
|
-
.widget-footer {
|
|
8745
|
+
.data-widget .widget-footer {
|
|
8120
8746
|
font-size: 12px;
|
|
8121
8747
|
color: #333;
|
|
8122
8748
|
}
|
|
@@ -8527,76 +9153,50 @@ ${SharedStyles}
|
|
|
8527
9153
|
|
|
8528
9154
|
// Subscribe to regular market data (Level 1)
|
|
8529
9155
|
// Create a wrapper to add data type context
|
|
8530
|
-
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'],
|
|
8531
|
-
const {
|
|
8532
|
-
event,
|
|
8533
|
-
data
|
|
8534
|
-
} = messageWrapper;
|
|
8535
|
-
|
|
8536
|
-
// Handle connection events
|
|
8537
|
-
if (event === 'connection') {
|
|
8538
|
-
this.handleConnectionStatus(data);
|
|
8539
|
-
return;
|
|
8540
|
-
}
|
|
8541
|
-
|
|
8542
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8543
|
-
if (event === 'data') {
|
|
8544
|
-
data._dataType = 'market';
|
|
8545
|
-
this.handleMessage({
|
|
8546
|
-
event,
|
|
8547
|
-
data
|
|
8548
|
-
});
|
|
8549
|
-
}
|
|
8550
|
-
}, this.symbol);
|
|
9156
|
+
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
|
|
8551
9157
|
|
|
8552
9158
|
// Subscribe to night session data (BlueOcean or Bruce)
|
|
8553
|
-
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'],
|
|
8554
|
-
const {
|
|
8555
|
-
event,
|
|
8556
|
-
data
|
|
8557
|
-
} = messageWrapper;
|
|
8558
|
-
|
|
8559
|
-
// Connection already handled by market subscription
|
|
8560
|
-
if (event === 'connection') {
|
|
8561
|
-
return;
|
|
8562
|
-
}
|
|
8563
|
-
|
|
8564
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8565
|
-
if (event === 'data') {
|
|
8566
|
-
data._dataType = 'night';
|
|
8567
|
-
this.handleMessage({
|
|
8568
|
-
event,
|
|
8569
|
-
data
|
|
8570
|
-
});
|
|
8571
|
-
}
|
|
8572
|
-
}, this.symbol);
|
|
9159
|
+
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
|
|
8573
9160
|
}
|
|
8574
9161
|
handleData(message) {
|
|
8575
9162
|
// Extract data type from metadata
|
|
8576
|
-
const dataType = message.
|
|
9163
|
+
const dataType = message.type;
|
|
8577
9164
|
if (this.debug) {
|
|
8578
9165
|
console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
|
|
8579
9166
|
}
|
|
8580
9167
|
|
|
8581
|
-
//
|
|
8582
|
-
if (
|
|
8583
|
-
|
|
8584
|
-
|
|
9168
|
+
// Clear loading timeout since we received data (use BaseWidget method)
|
|
9169
|
+
if (this.loadingTimeout) {
|
|
9170
|
+
this.clearTimeout(this.loadingTimeout);
|
|
9171
|
+
this.loadingTimeout = null;
|
|
9172
|
+
}
|
|
9173
|
+
|
|
9174
|
+
// Handle option chain data
|
|
9175
|
+
if (Array.isArray(message)) {
|
|
9176
|
+
if (message.length === 0) {
|
|
9177
|
+
// Only show no data state if we don't have cached data
|
|
9178
|
+
if (!this.data) {
|
|
9179
|
+
this.showNoDataState();
|
|
9180
|
+
} else {
|
|
9181
|
+
this.hideLoading();
|
|
9182
|
+
if (this.debug) {
|
|
9183
|
+
console.log('[OptionChainWidget] No new data, keeping cached data visible');
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
8585
9186
|
}
|
|
8586
|
-
return;
|
|
8587
9187
|
}
|
|
8588
9188
|
|
|
8589
9189
|
// Handle error messages from server
|
|
8590
|
-
if (message.type === 'error'
|
|
9190
|
+
if (message.type === 'error' || message.error == true) {
|
|
8591
9191
|
if (this.debug) {
|
|
8592
9192
|
console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
|
|
8593
9193
|
}
|
|
8594
9194
|
// Keep existing cached data visible, just hide loading
|
|
8595
|
-
if (dataType === '
|
|
9195
|
+
if (dataType === 'queryl1' && !this.marketData) {
|
|
8596
9196
|
if (this.debug) {
|
|
8597
9197
|
console.log('[CombinedMarketWidget] No market data available');
|
|
8598
9198
|
}
|
|
8599
|
-
} else if (dataType === '
|
|
9199
|
+
} else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
|
|
8600
9200
|
if (this.debug) {
|
|
8601
9201
|
console.log('[CombinedMarketWidget] No night session data available');
|
|
8602
9202
|
}
|
|
@@ -9049,7 +9649,7 @@ ${SharedStyles}
|
|
|
9049
9649
|
margin: 0 auto;
|
|
9050
9650
|
}
|
|
9051
9651
|
|
|
9052
|
-
.chart-header {
|
|
9652
|
+
.intraday-chart-widget .chart-header {
|
|
9053
9653
|
display: flex;
|
|
9054
9654
|
justify-content: space-between;
|
|
9055
9655
|
align-items: center;
|
|
@@ -9058,13 +9658,13 @@ ${SharedStyles}
|
|
|
9058
9658
|
border-bottom: 1px solid #e5e7eb;
|
|
9059
9659
|
}
|
|
9060
9660
|
|
|
9061
|
-
.chart-title-section {
|
|
9661
|
+
.intraday-chart-widget .chart-title-section {
|
|
9062
9662
|
display: flex;
|
|
9063
9663
|
flex-direction: column;
|
|
9064
9664
|
gap: 4px;
|
|
9065
9665
|
}
|
|
9066
9666
|
|
|
9067
|
-
.company-market-info {
|
|
9667
|
+
.intraday-chart-widget .company-market-info {
|
|
9068
9668
|
display: flex;
|
|
9069
9669
|
gap: 8px;
|
|
9070
9670
|
align-items: center;
|
|
@@ -9072,18 +9672,18 @@ ${SharedStyles}
|
|
|
9072
9672
|
color: #6b7280;
|
|
9073
9673
|
}
|
|
9074
9674
|
|
|
9075
|
-
.intraday-company-name {
|
|
9675
|
+
.intraday-chart-widget .intraday-company-name {
|
|
9076
9676
|
font-weight: 500;
|
|
9077
9677
|
}
|
|
9078
9678
|
|
|
9079
|
-
.intraday-chart-symbol {
|
|
9679
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
9080
9680
|
font-size: 1.5em;
|
|
9081
9681
|
font-weight: 700;
|
|
9082
9682
|
color: #1f2937;
|
|
9083
9683
|
margin: 0;
|
|
9084
9684
|
}
|
|
9085
9685
|
|
|
9086
|
-
.intraday-chart-source {
|
|
9686
|
+
.intraday-chart-widget .intraday-chart-source {
|
|
9087
9687
|
font-size: 0.75em;
|
|
9088
9688
|
padding: 4px 8px;
|
|
9089
9689
|
border-radius: 4px;
|
|
@@ -9092,36 +9692,36 @@ ${SharedStyles}
|
|
|
9092
9692
|
font-weight: 600;
|
|
9093
9693
|
}
|
|
9094
9694
|
|
|
9095
|
-
.chart-change {
|
|
9695
|
+
.intraday-chart-widget .chart-change {
|
|
9096
9696
|
font-size: 1.1em;
|
|
9097
9697
|
font-weight: 600;
|
|
9098
9698
|
padding: 6px 12px;
|
|
9099
9699
|
border-radius: 6px;
|
|
9100
9700
|
}
|
|
9101
9701
|
|
|
9102
|
-
.chart-change.positive {
|
|
9702
|
+
.intraday-chart-widget .chart-change.positive {
|
|
9103
9703
|
color: #059669;
|
|
9104
9704
|
background: #d1fae5;
|
|
9105
9705
|
}
|
|
9106
9706
|
|
|
9107
|
-
.chart-change.negative {
|
|
9707
|
+
.intraday-chart-widget .chart-change.negative {
|
|
9108
9708
|
color: #dc2626;
|
|
9109
9709
|
background: #fee2e2;
|
|
9110
9710
|
}
|
|
9111
9711
|
|
|
9112
|
-
.chart-controls {
|
|
9712
|
+
.intraday-chart-widget .chart-controls {
|
|
9113
9713
|
display: flex;
|
|
9114
9714
|
justify-content: space-between;
|
|
9115
9715
|
align-items: center;
|
|
9116
9716
|
margin-bottom: 15px;
|
|
9117
9717
|
}
|
|
9118
9718
|
|
|
9119
|
-
.chart-range-selector {
|
|
9719
|
+
.intraday-chart-widget .chart-range-selector {
|
|
9120
9720
|
display: flex;
|
|
9121
9721
|
gap: 8px;
|
|
9122
9722
|
}
|
|
9123
9723
|
|
|
9124
|
-
.range-btn {
|
|
9724
|
+
.intraday-chart-widget .range-btn {
|
|
9125
9725
|
padding: 8px 16px;
|
|
9126
9726
|
border: 1px solid #e5e7eb;
|
|
9127
9727
|
background: white;
|
|
@@ -9133,28 +9733,28 @@ ${SharedStyles}
|
|
|
9133
9733
|
transition: all 0.2s ease;
|
|
9134
9734
|
}
|
|
9135
9735
|
|
|
9136
|
-
.range-btn:hover {
|
|
9736
|
+
.intraday-chart-widget .range-btn:hover {
|
|
9137
9737
|
background: #f9fafb;
|
|
9138
9738
|
border-color: #d1d5db;
|
|
9139
9739
|
}
|
|
9140
9740
|
|
|
9141
|
-
.range-btn.active {
|
|
9741
|
+
.intraday-chart-widget .range-btn.active {
|
|
9142
9742
|
background: #667eea;
|
|
9143
9743
|
color: white;
|
|
9144
9744
|
border-color: #667eea;
|
|
9145
9745
|
}
|
|
9146
9746
|
|
|
9147
|
-
.range-btn:focus {
|
|
9747
|
+
.intraday-chart-widget .range-btn:focus {
|
|
9148
9748
|
outline: none;
|
|
9149
9749
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
9150
9750
|
}
|
|
9151
9751
|
|
|
9152
|
-
.chart-type-selector {
|
|
9752
|
+
.intraday-chart-widget .chart-type-selector {
|
|
9153
9753
|
display: flex;
|
|
9154
9754
|
gap: 8px;
|
|
9155
9755
|
}
|
|
9156
9756
|
|
|
9157
|
-
.type-btn {
|
|
9757
|
+
.intraday-chart-widget .type-btn {
|
|
9158
9758
|
padding: 8px 16px;
|
|
9159
9759
|
border: 1px solid #e5e7eb;
|
|
9160
9760
|
background: white;
|
|
@@ -9166,23 +9766,23 @@ ${SharedStyles}
|
|
|
9166
9766
|
transition: all 0.2s ease;
|
|
9167
9767
|
}
|
|
9168
9768
|
|
|
9169
|
-
.type-btn:hover {
|
|
9769
|
+
.intraday-chart-widget .type-btn:hover {
|
|
9170
9770
|
background: #f9fafb;
|
|
9171
9771
|
border-color: #d1d5db;
|
|
9172
9772
|
}
|
|
9173
9773
|
|
|
9174
|
-
.type-btn.active {
|
|
9774
|
+
.intraday-chart-widget .type-btn.active {
|
|
9175
9775
|
background: #10b981;
|
|
9176
9776
|
color: white;
|
|
9177
9777
|
border-color: #10b981;
|
|
9178
9778
|
}
|
|
9179
9779
|
|
|
9180
|
-
.type-btn:focus {
|
|
9780
|
+
.intraday-chart-widget .type-btn:focus {
|
|
9181
9781
|
outline: none;
|
|
9182
9782
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
|
9183
9783
|
}
|
|
9184
9784
|
|
|
9185
|
-
.zoom-reset-btn {
|
|
9785
|
+
.intraday-chart-widget .zoom-reset-btn {
|
|
9186
9786
|
display: flex;
|
|
9187
9787
|
align-items: center;
|
|
9188
9788
|
gap: 6px;
|
|
@@ -9197,34 +9797,34 @@ ${SharedStyles}
|
|
|
9197
9797
|
transition: all 0.2s ease;
|
|
9198
9798
|
}
|
|
9199
9799
|
|
|
9200
|
-
.zoom-reset-btn:hover {
|
|
9800
|
+
.intraday-chart-widget .zoom-reset-btn:hover {
|
|
9201
9801
|
background: #f9fafb;
|
|
9202
9802
|
border-color: #667eea;
|
|
9203
9803
|
color: #667eea;
|
|
9204
9804
|
}
|
|
9205
9805
|
|
|
9206
|
-
.zoom-reset-btn:focus {
|
|
9806
|
+
.intraday-chart-widget .zoom-reset-btn:focus {
|
|
9207
9807
|
outline: none;
|
|
9208
9808
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
9209
9809
|
}
|
|
9210
9810
|
|
|
9211
|
-
.zoom-reset-btn svg {
|
|
9811
|
+
.intraday-chart-widget .zoom-reset-btn svg {
|
|
9212
9812
|
flex-shrink: 0;
|
|
9213
9813
|
}
|
|
9214
9814
|
|
|
9215
|
-
.chart-container {
|
|
9815
|
+
.intraday-chart-widget .chart-container {
|
|
9216
9816
|
height: 500px;
|
|
9217
9817
|
margin-bottom: 20px;
|
|
9218
9818
|
position: relative;
|
|
9219
9819
|
}
|
|
9220
9820
|
|
|
9221
|
-
.chart-stats {
|
|
9821
|
+
.intraday-chart-widget .chart-stats {
|
|
9222
9822
|
padding: 15px;
|
|
9223
9823
|
background: #f9fafb;
|
|
9224
9824
|
border-radius: 8px;
|
|
9225
9825
|
}
|
|
9226
9826
|
|
|
9227
|
-
.stats-header {
|
|
9827
|
+
.intraday-chart-widget .stats-header {
|
|
9228
9828
|
font-size: 0.875em;
|
|
9229
9829
|
font-weight: 700;
|
|
9230
9830
|
color: #374151;
|
|
@@ -9235,19 +9835,19 @@ ${SharedStyles}
|
|
|
9235
9835
|
border-bottom: 2px solid #e5e7eb;
|
|
9236
9836
|
}
|
|
9237
9837
|
|
|
9238
|
-
.stats-grid {
|
|
9838
|
+
.intraday-chart-widget .stats-grid {
|
|
9239
9839
|
display: grid;
|
|
9240
9840
|
grid-template-columns: repeat(5, 1fr);
|
|
9241
9841
|
gap: 15px;
|
|
9242
9842
|
}
|
|
9243
9843
|
|
|
9244
|
-
.stat-item {
|
|
9844
|
+
.intraday-chart-widget .stat-item {
|
|
9245
9845
|
display: flex;
|
|
9246
9846
|
flex-direction: column;
|
|
9247
9847
|
gap: 4px;
|
|
9248
9848
|
}
|
|
9249
9849
|
|
|
9250
|
-
.stat-label {
|
|
9850
|
+
.intraday-chart-widget .stat-label {
|
|
9251
9851
|
font-size: 0.75em;
|
|
9252
9852
|
color: #6b7280;
|
|
9253
9853
|
font-weight: 600;
|
|
@@ -9255,13 +9855,13 @@ ${SharedStyles}
|
|
|
9255
9855
|
letter-spacing: 0.5px;
|
|
9256
9856
|
}
|
|
9257
9857
|
|
|
9258
|
-
.stat-value {
|
|
9858
|
+
.intraday-chart-widget .stat-value {
|
|
9259
9859
|
font-size: 1.1em;
|
|
9260
9860
|
font-weight: 700;
|
|
9261
9861
|
color: #1f2937;
|
|
9262
9862
|
}
|
|
9263
9863
|
|
|
9264
|
-
.widget-loading-overlay {
|
|
9864
|
+
.intraday-chart-widget .widget-loading-overlay {
|
|
9265
9865
|
position: absolute;
|
|
9266
9866
|
top: 0;
|
|
9267
9867
|
left: 0;
|
|
@@ -9280,26 +9880,26 @@ ${SharedStyles}
|
|
|
9280
9880
|
backdrop-filter: blur(1px);
|
|
9281
9881
|
}
|
|
9282
9882
|
|
|
9283
|
-
.widget-loading-overlay.hidden {
|
|
9883
|
+
.intraday-chart-widget .widget-loading-overlay.hidden {
|
|
9284
9884
|
display: none;
|
|
9285
9885
|
}
|
|
9286
9886
|
|
|
9287
|
-
.loading-spinner {
|
|
9887
|
+
.intraday-chart-widget .loading-spinner {
|
|
9288
9888
|
width: 20px;
|
|
9289
9889
|
height: 20px;
|
|
9290
9890
|
border: 3px solid #e5e7eb;
|
|
9291
9891
|
border-top-color: #667eea;
|
|
9292
9892
|
border-radius: 50%;
|
|
9293
|
-
animation: spin 0.8s linear infinite;
|
|
9893
|
+
animation: intraday-spin 0.8s linear infinite;
|
|
9294
9894
|
}
|
|
9295
9895
|
|
|
9296
|
-
.loading-text {
|
|
9896
|
+
.intraday-chart-widget .loading-text {
|
|
9297
9897
|
color: #6b7280;
|
|
9298
9898
|
font-size: 0.875em;
|
|
9299
9899
|
font-weight: 500;
|
|
9300
9900
|
}
|
|
9301
9901
|
|
|
9302
|
-
@keyframes spin {
|
|
9902
|
+
@keyframes intraday-spin {
|
|
9303
9903
|
to { transform: rotate(360deg); }
|
|
9304
9904
|
}
|
|
9305
9905
|
|
|
@@ -9309,26 +9909,26 @@ ${SharedStyles}
|
|
|
9309
9909
|
padding: 15px;
|
|
9310
9910
|
}
|
|
9311
9911
|
|
|
9312
|
-
.stats-grid {
|
|
9912
|
+
.intraday-chart-widget .stats-grid {
|
|
9313
9913
|
grid-template-columns: repeat(3, 1fr);
|
|
9314
9914
|
gap: 10px;
|
|
9315
9915
|
}
|
|
9316
9916
|
|
|
9317
|
-
.stats-header {
|
|
9917
|
+
.intraday-chart-widget .stats-header {
|
|
9318
9918
|
font-size: 0.8em;
|
|
9319
9919
|
margin-bottom: 10px;
|
|
9320
9920
|
}
|
|
9321
9921
|
|
|
9322
|
-
.chart-container {
|
|
9922
|
+
.intraday-chart-widget .chart-container {
|
|
9323
9923
|
height: 350px;
|
|
9324
9924
|
}
|
|
9325
9925
|
|
|
9326
|
-
.intraday-chart-symbol {
|
|
9926
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
9327
9927
|
font-size: 1.2em;
|
|
9328
9928
|
}
|
|
9329
9929
|
}
|
|
9330
9930
|
|
|
9331
|
-
.widget-error {
|
|
9931
|
+
.intraday-chart-widget .widget-error {
|
|
9332
9932
|
padding: 15px;
|
|
9333
9933
|
background: #fee2e2;
|
|
9334
9934
|
border: 1px solid #fecaca;
|
|
@@ -38704,7 +39304,20 @@ ${SharedStyles}
|
|
|
38704
39304
|
}
|
|
38705
39305
|
async initialize() {
|
|
38706
39306
|
// Validate initial symbol via API to get company info
|
|
38707
|
-
|
|
39307
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
39308
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
39309
|
+
// Extract company info from first data item
|
|
39310
|
+
if (data && data[0]) {
|
|
39311
|
+
this.companyName = data[0].comp_name || '';
|
|
39312
|
+
this.exchangeName = data[0].market_name || '';
|
|
39313
|
+
this.mic = data[0].mic || '';
|
|
39314
|
+
}
|
|
39315
|
+
});
|
|
39316
|
+
|
|
39317
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
39318
|
+
if (validationSuccess === false) {
|
|
39319
|
+
return; // Error is already shown, don't continue
|
|
39320
|
+
}
|
|
38708
39321
|
|
|
38709
39322
|
// Update company name in the header
|
|
38710
39323
|
this.updateCompanyName();
|
|
@@ -38712,38 +39325,6 @@ ${SharedStyles}
|
|
|
38712
39325
|
// Load chart data
|
|
38713
39326
|
await this.loadChartData();
|
|
38714
39327
|
}
|
|
38715
|
-
async validateInitialSymbol() {
|
|
38716
|
-
try {
|
|
38717
|
-
const apiService = this.wsManager.getApiService();
|
|
38718
|
-
if (!apiService) {
|
|
38719
|
-
if (this.debug) {
|
|
38720
|
-
console.log('[IntradayChartWidget] API service not available for initial validation');
|
|
38721
|
-
}
|
|
38722
|
-
return;
|
|
38723
|
-
}
|
|
38724
|
-
const result = await apiService.quotel1(this.symbol);
|
|
38725
|
-
if (result && result.data && result.data[0]) {
|
|
38726
|
-
const symbolData = result.data[0];
|
|
38727
|
-
// Extract company info
|
|
38728
|
-
this.companyName = symbolData.comp_name || '';
|
|
38729
|
-
this.exchangeName = symbolData.market_name || '';
|
|
38730
|
-
this.mic = symbolData.mic || '';
|
|
38731
|
-
if (this.debug) {
|
|
38732
|
-
console.log('[IntradayChartWidget] Initial symbol validated:', {
|
|
38733
|
-
symbol: this.symbol,
|
|
38734
|
-
companyName: this.companyName,
|
|
38735
|
-
exchangeName: this.exchangeName,
|
|
38736
|
-
mic: this.mic
|
|
38737
|
-
});
|
|
38738
|
-
}
|
|
38739
|
-
}
|
|
38740
|
-
} catch (error) {
|
|
38741
|
-
if (this.debug) {
|
|
38742
|
-
console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
|
|
38743
|
-
}
|
|
38744
|
-
// Don't throw - let the widget continue with chart data
|
|
38745
|
-
}
|
|
38746
|
-
}
|
|
38747
39328
|
updateCompanyName() {
|
|
38748
39329
|
const companyNameElement = this.container.querySelector('.intraday-company-name');
|
|
38749
39330
|
if (companyNameElement) {
|
|
@@ -39167,19 +39748,12 @@ ${SharedStyles}
|
|
|
39167
39748
|
// Handle array format (standard night session format)
|
|
39168
39749
|
if (Array.isArray(message.data)) {
|
|
39169
39750
|
console.log('[IntradayChartWidget] Processing array format, length:', message.length);
|
|
39170
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
39751
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
39171
39752
|
console.log('[IntradayChartWidget] Found symbol data:', symbolData);
|
|
39172
39753
|
if (symbolData && !symbolData.NotFound) {
|
|
39173
39754
|
priceData = symbolData;
|
|
39174
39755
|
}
|
|
39175
39756
|
}
|
|
39176
|
-
// Handle wrapped format
|
|
39177
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
|
|
39178
|
-
console.log('[IntradayChartWidget] Processing wrapped format');
|
|
39179
|
-
if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
|
|
39180
|
-
priceData = message['0'];
|
|
39181
|
-
}
|
|
39182
|
-
}
|
|
39183
39757
|
// Handle direct data format
|
|
39184
39758
|
else if (message.Symbol === this.symbol && !message.NotFound) {
|
|
39185
39759
|
console.log('[IntradayChartWidget] Processing direct format');
|
|
@@ -40282,7 +40856,19 @@ ${SharedStyles}
|
|
|
40282
40856
|
this.showLoading();
|
|
40283
40857
|
|
|
40284
40858
|
// Fetch company info for initial symbol
|
|
40285
|
-
|
|
40859
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40860
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40861
|
+
// Extract company info from first data item
|
|
40862
|
+
if (data && data[0]) {
|
|
40863
|
+
this.companyName = data[0].comp_name || '';
|
|
40864
|
+
this.exchangeName = data[0].market_name || '';
|
|
40865
|
+
}
|
|
40866
|
+
});
|
|
40867
|
+
|
|
40868
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40869
|
+
if (validationSuccess === false) {
|
|
40870
|
+
return; // Error is already shown, don't continue
|
|
40871
|
+
}
|
|
40286
40872
|
|
|
40287
40873
|
// Set timeout to detect no data on initial load
|
|
40288
40874
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40294,36 +40880,6 @@ ${SharedStyles}
|
|
|
40294
40880
|
}, 10000);
|
|
40295
40881
|
this.subscribeToData();
|
|
40296
40882
|
}
|
|
40297
|
-
async validateInitialSymbol() {
|
|
40298
|
-
try {
|
|
40299
|
-
const apiService = this.wsManager.getApiService();
|
|
40300
|
-
if (!apiService) {
|
|
40301
|
-
if (this.debug) {
|
|
40302
|
-
console.log('[ONBBO L2] API service not available for initial validation');
|
|
40303
|
-
}
|
|
40304
|
-
return;
|
|
40305
|
-
}
|
|
40306
|
-
const result = await apiService.quotel1(this.symbol);
|
|
40307
|
-
if (result && result.data && result.data[0]) {
|
|
40308
|
-
const symbolData = result.data[0];
|
|
40309
|
-
// Extract company info
|
|
40310
|
-
this.companyName = symbolData.comp_name || '';
|
|
40311
|
-
this.exchangeName = symbolData.market_name || '';
|
|
40312
|
-
if (this.debug) {
|
|
40313
|
-
console.log('[ONBBO L2] Initial symbol validated:', {
|
|
40314
|
-
symbol: this.symbol,
|
|
40315
|
-
companyName: this.companyName,
|
|
40316
|
-
exchangeName: this.exchangeName
|
|
40317
|
-
});
|
|
40318
|
-
}
|
|
40319
|
-
}
|
|
40320
|
-
} catch (error) {
|
|
40321
|
-
if (this.debug) {
|
|
40322
|
-
console.warn('[ONBBO L2] Initial symbol validation failed:', error);
|
|
40323
|
-
}
|
|
40324
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
40325
|
-
}
|
|
40326
|
-
}
|
|
40327
40883
|
subscribeToData() {
|
|
40328
40884
|
// Subscribe to ONBBO Level 2 data (order book)
|
|
40329
40885
|
// Use separate widget IDs to avoid "already active" duplicate detection
|
|
@@ -40842,6 +41398,7 @@ ${SharedStyles}
|
|
|
40842
41398
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40843
41399
|
this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
|
|
40844
41400
|
this.data = new TimeSalesModel([], this.maxTrades);
|
|
41401
|
+
this.data.symbol = this.symbol; // Set the symbol in the data model
|
|
40845
41402
|
this.isDestroyed = false;
|
|
40846
41403
|
this.unsubscribe = null;
|
|
40847
41404
|
this.symbolEditor = null;
|
|
@@ -40853,6 +41410,7 @@ ${SharedStyles}
|
|
|
40853
41410
|
|
|
40854
41411
|
// Create widget structure
|
|
40855
41412
|
this.createWidgetStructure();
|
|
41413
|
+
this.initializeSymbolEditor();
|
|
40856
41414
|
|
|
40857
41415
|
// Initialize the widget
|
|
40858
41416
|
this.initialize();
|
|
@@ -40869,7 +41427,26 @@ ${SharedStyles}
|
|
|
40869
41427
|
}
|
|
40870
41428
|
}
|
|
40871
41429
|
this.addStyles();
|
|
40872
|
-
this.setupSymbolEditor();
|
|
41430
|
+
//this.setupSymbolEditor();
|
|
41431
|
+
}
|
|
41432
|
+
initializeSymbolEditor() {
|
|
41433
|
+
// Initialize symbol editor with stock symbol validation
|
|
41434
|
+
this.symbolEditor = new SymbolEditor(this, {
|
|
41435
|
+
maxLength: 10,
|
|
41436
|
+
placeholder: 'Enter symbol...',
|
|
41437
|
+
//validator: this.validateStockSymbol.bind(this),
|
|
41438
|
+
onSymbolChange: this.handleSymbolChange.bind(this),
|
|
41439
|
+
debug: this.debug,
|
|
41440
|
+
autoUppercase: true,
|
|
41441
|
+
symbolType: 'quotel1'
|
|
41442
|
+
});
|
|
41443
|
+
|
|
41444
|
+
// Set initial symbol
|
|
41445
|
+
const symbolElement = this.container.querySelector('.symbol');
|
|
41446
|
+
if (symbolElement) {
|
|
41447
|
+
symbolElement.textContent = this.symbol;
|
|
41448
|
+
symbolElement.dataset.originalSymbol = this.symbol;
|
|
41449
|
+
}
|
|
40873
41450
|
}
|
|
40874
41451
|
addStyles() {
|
|
40875
41452
|
// Inject styles from the styles file into document head
|
|
@@ -40892,6 +41469,8 @@ ${SharedStyles}
|
|
|
40892
41469
|
// Keep editor open when clicking buttons
|
|
40893
41470
|
symbolType: 'quotel1' // Use standard quote validation
|
|
40894
41471
|
});
|
|
41472
|
+
|
|
41473
|
+
//console.log('SETUP COMPLETE')
|
|
40895
41474
|
}
|
|
40896
41475
|
async handleSymbolChange(newSymbol, oldSymbol) {
|
|
40897
41476
|
let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
@@ -40994,7 +41573,19 @@ ${SharedStyles}
|
|
|
40994
41573
|
this.showLoading();
|
|
40995
41574
|
|
|
40996
41575
|
// Validate initial symbol via API to get company info
|
|
40997
|
-
|
|
41576
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
41577
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
41578
|
+
// Extract company info from first data item
|
|
41579
|
+
if (data && data[0]) {
|
|
41580
|
+
this.companyName = data[0].comp_name || '';
|
|
41581
|
+
this.exchangeName = data[0].market_name || '';
|
|
41582
|
+
}
|
|
41583
|
+
});
|
|
41584
|
+
|
|
41585
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
41586
|
+
if (validationSuccess === false) {
|
|
41587
|
+
return; // Error is already shown, don't continue
|
|
41588
|
+
}
|
|
40998
41589
|
|
|
40999
41590
|
// Set timeout to detect no data on initial load
|
|
41000
41591
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -41007,36 +41598,6 @@ ${SharedStyles}
|
|
|
41007
41598
|
|
|
41008
41599
|
this.subscribeToData();
|
|
41009
41600
|
}
|
|
41010
|
-
async validateInitialSymbol() {
|
|
41011
|
-
try {
|
|
41012
|
-
const apiService = this.wsManager.getApiService();
|
|
41013
|
-
if (!apiService) {
|
|
41014
|
-
if (this.debug) {
|
|
41015
|
-
console.log('[TimeSalesWidget] API service not available for initial validation');
|
|
41016
|
-
}
|
|
41017
|
-
return;
|
|
41018
|
-
}
|
|
41019
|
-
const result = await apiService.quotel1(this.symbol);
|
|
41020
|
-
if (result && result.data && result.data[0]) {
|
|
41021
|
-
const symbolData = result.data[0];
|
|
41022
|
-
// Extract company info
|
|
41023
|
-
this.companyName = symbolData.comp_name || '';
|
|
41024
|
-
this.exchangeName = symbolData.market_name || '';
|
|
41025
|
-
if (this.debug) {
|
|
41026
|
-
console.log('[TimeSalesWidget] Initial symbol validated:', {
|
|
41027
|
-
symbol: this.symbol,
|
|
41028
|
-
companyName: this.companyName,
|
|
41029
|
-
exchangeName: this.exchangeName
|
|
41030
|
-
});
|
|
41031
|
-
}
|
|
41032
|
-
}
|
|
41033
|
-
} catch (error) {
|
|
41034
|
-
if (this.debug) {
|
|
41035
|
-
console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
|
|
41036
|
-
}
|
|
41037
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
41038
|
-
}
|
|
41039
|
-
}
|
|
41040
41601
|
subscribeToData() {
|
|
41041
41602
|
// Subscribe to Time & Sales data (using queryonbbotimesale)
|
|
41042
41603
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
|
|
@@ -41052,6 +41613,9 @@ ${SharedStyles}
|
|
|
41052
41613
|
clearTimeout(this.loadingTimeout);
|
|
41053
41614
|
this.loadingTimeout = null;
|
|
41054
41615
|
}
|
|
41616
|
+
if (message.type != 'queryonbbotimesale') {
|
|
41617
|
+
return;
|
|
41618
|
+
}
|
|
41055
41619
|
|
|
41056
41620
|
// Handle error messages
|
|
41057
41621
|
if (message.type === 'error') {
|
|
@@ -41064,30 +41628,16 @@ ${SharedStyles}
|
|
|
41064
41628
|
}
|
|
41065
41629
|
|
|
41066
41630
|
// Handle Time & Sales data - Array of trades
|
|
41067
|
-
if (Array.isArray(message)) {
|
|
41631
|
+
if (Array.isArray(message.data)) {
|
|
41068
41632
|
if (this.debug) {
|
|
41069
41633
|
console.log('[TimeSalesWidget] Received trades array:', message);
|
|
41070
41634
|
}
|
|
41071
41635
|
|
|
41072
41636
|
// Add new trades to existing data
|
|
41073
|
-
this.data.addTrades(message);
|
|
41637
|
+
this.data.addTrades(message.data);
|
|
41074
41638
|
this.updateWidget();
|
|
41075
41639
|
return;
|
|
41076
41640
|
}
|
|
41077
|
-
|
|
41078
|
-
// Handle wrapped format
|
|
41079
|
-
if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
|
|
41080
|
-
if (Array.isArray(message['0'])) {
|
|
41081
|
-
if (this.debug) {
|
|
41082
|
-
console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
|
|
41083
|
-
}
|
|
41084
|
-
|
|
41085
|
-
// Add new trades to existing data
|
|
41086
|
-
this.data.addTrades(message['0']);
|
|
41087
|
-
this.updateWidget();
|
|
41088
|
-
return;
|
|
41089
|
-
}
|
|
41090
|
-
}
|
|
41091
41641
|
if (this.debug) {
|
|
41092
41642
|
console.log('[TimeSalesWidget] Unexpected message format:', message);
|
|
41093
41643
|
}
|
|
@@ -41314,7 +41864,15 @@ ${SharedStyles}
|
|
|
41314
41864
|
try {
|
|
41315
41865
|
const response = await fetch(url, config);
|
|
41316
41866
|
if (!response.ok) {
|
|
41317
|
-
|
|
41867
|
+
// Try to get response body for error details
|
|
41868
|
+
let errorBody = '';
|
|
41869
|
+
try {
|
|
41870
|
+
errorBody = await response.text();
|
|
41871
|
+
console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
|
|
41872
|
+
} catch (e) {
|
|
41873
|
+
console.error(`[ApiService] Could not read error response body:`, e);
|
|
41874
|
+
}
|
|
41875
|
+
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
|
|
41318
41876
|
}
|
|
41319
41877
|
return await response.json();
|
|
41320
41878
|
} catch (error) {
|
|
@@ -42074,8 +42632,8 @@ ${SharedStyles}
|
|
|
42074
42632
|
*/
|
|
42075
42633
|
_normalizeMessage(message) {
|
|
42076
42634
|
// Check for error field (case-insensitive)
|
|
42077
|
-
const errorField = message.error
|
|
42078
|
-
const typeField = message.type
|
|
42635
|
+
const errorField = message.error;
|
|
42636
|
+
const typeField = message.type;
|
|
42079
42637
|
const messageField = message.message || message.Message;
|
|
42080
42638
|
|
|
42081
42639
|
// Handle explicit error messages - route based on type field
|
|
@@ -42115,54 +42673,16 @@ ${SharedStyles}
|
|
|
42115
42673
|
}
|
|
42116
42674
|
}
|
|
42117
42675
|
|
|
42118
|
-
// Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
|
|
42119
|
-
const dataField = message.data || message.Data;
|
|
42120
|
-
if (typeField && dataField !== undefined) {
|
|
42121
|
-
if (this.config.debug) {
|
|
42122
|
-
console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
|
|
42123
|
-
}
|
|
42124
|
-
|
|
42125
|
-
// Extract the actual data and add the type to it for routing
|
|
42126
|
-
let normalizedData;
|
|
42127
|
-
if (Array.isArray(dataField)) {
|
|
42128
|
-
// If data is an array, process each item
|
|
42129
|
-
normalizedData = dataField;
|
|
42130
|
-
// Add type info to the structure for routing
|
|
42131
|
-
if (normalizedData.length > 0) {
|
|
42132
|
-
normalizedData._messageType = typeField;
|
|
42133
|
-
}
|
|
42134
|
-
} else if (typeof dataField === 'object' && dataField !== null) {
|
|
42135
|
-
// If data is an object, use it directly
|
|
42136
|
-
normalizedData = dataField;
|
|
42137
|
-
// Add type info for routing
|
|
42138
|
-
normalizedData._messageType = typeField;
|
|
42139
|
-
} else {
|
|
42140
|
-
// Primitive value, wrap it
|
|
42141
|
-
normalizedData = {
|
|
42142
|
-
value: dataField,
|
|
42143
|
-
_messageType: typeField
|
|
42144
|
-
};
|
|
42145
|
-
}
|
|
42146
|
-
|
|
42147
|
-
// Only add type field if the normalized data doesn't already have one
|
|
42148
|
-
if (typeof normalizedData === 'object' && !normalizedData.type && !normalizedData.Type) {
|
|
42149
|
-
normalizedData.type = typeField;
|
|
42150
|
-
}
|
|
42151
|
-
return normalizedData;
|
|
42152
|
-
}
|
|
42153
|
-
|
|
42154
42676
|
// Old format or no type/data structure, return as is
|
|
42155
42677
|
return message;
|
|
42156
42678
|
}
|
|
42157
42679
|
_routeMessage(message) {
|
|
42158
42680
|
//console.log('message', message);
|
|
42159
42681
|
|
|
42160
|
-
|
|
42161
|
-
|
|
42162
|
-
|
|
42163
|
-
|
|
42164
|
-
return; // Don't route empty arrays
|
|
42165
|
-
}
|
|
42682
|
+
// IMPORTANT: Don't filter out empty arrays here - route them based on message.type
|
|
42683
|
+
// Widgets need to receive empty arrays to clear loading states and show "no data" messages
|
|
42684
|
+
// The message structure is: { type: 'queryoptionchain', data: [] }
|
|
42685
|
+
// We need to route based on the 'type' field, not the data content
|
|
42166
42686
|
|
|
42167
42687
|
// Cache the message for later use by new subscribers
|
|
42168
42688
|
this._cacheMessage(message);
|
|
@@ -42261,27 +42781,34 @@ ${SharedStyles}
|
|
|
42261
42781
|
_getRelevantWidgets(message) {
|
|
42262
42782
|
const relevantWidgets = new Set();
|
|
42263
42783
|
|
|
42784
|
+
// OPTIMIZATION: Extract message type once from the entire message, not from each data item
|
|
42785
|
+
// Message structure: { type: 'queryoptionchain', data: [] }
|
|
42786
|
+
let dataField = message.Data || message.data;
|
|
42787
|
+
const messageType = message.type || this._extractMessageType(dataField);
|
|
42788
|
+
|
|
42264
42789
|
// Handle array messages
|
|
42265
|
-
this._addRelevantWidgetsForItem(message, relevantWidgets);
|
|
42790
|
+
this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
|
|
42266
42791
|
return relevantWidgets;
|
|
42267
42792
|
}
|
|
42268
|
-
_addRelevantWidgetsForItem(item, relevantWidgets) {
|
|
42793
|
+
_addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
|
|
42269
42794
|
// If item has Data array, process it
|
|
42270
42795
|
if (item.Data && Array.isArray(item.Data)) {
|
|
42271
42796
|
//console.log('Data array found with length:', item.Data.length);
|
|
42272
42797
|
item.Data.forEach(dataItem => {
|
|
42273
|
-
|
|
42798
|
+
// Pass messageType from parent message, don't extract from each data item
|
|
42799
|
+
this._processDataItem(dataItem, relevantWidgets, messageType);
|
|
42274
42800
|
});
|
|
42275
|
-
} else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
|
|
42276
|
-
this._processOptionChainData(item, relevantWidgets);
|
|
42801
|
+
} else if (item.data && item.data[0] && item.data[0].Strike !== undefined && item.data[0].Expire && !item.data[0].underlyingSymbol) {
|
|
42802
|
+
this._processOptionChainData(item.data, relevantWidgets);
|
|
42277
42803
|
} else {
|
|
42278
|
-
// Process single item
|
|
42279
|
-
this._processDataItem(item, relevantWidgets);
|
|
42804
|
+
// Process single item - messageType already extracted from message
|
|
42805
|
+
this._processDataItem(item, relevantWidgets, messageType);
|
|
42280
42806
|
}
|
|
42281
42807
|
}
|
|
42282
42808
|
|
|
42283
42809
|
// Simplified option chain processing - handle entire array at once
|
|
42284
42810
|
_processOptionChainData(optionChainArray, relevantWidgets) {
|
|
42811
|
+
//console.log('PROCESSING DATA OPTIONS')
|
|
42285
42812
|
if (!optionChainArray || optionChainArray.length === 0) return;
|
|
42286
42813
|
// Extract underlying symbol and date from first option contract
|
|
42287
42814
|
const firstOption = optionChainArray[0];
|
|
@@ -42320,12 +42847,11 @@ ${SharedStyles}
|
|
|
42320
42847
|
}
|
|
42321
42848
|
}
|
|
42322
42849
|
}
|
|
42323
|
-
_processDataItem(dataItem, relevantWidgets) {
|
|
42850
|
+
_processDataItem(dataItem, relevantWidgets, messageType) {
|
|
42324
42851
|
// Process individual data items and route to appropriate widgets
|
|
42325
42852
|
// Option chain arrays are handled separately by _processOptionChainData
|
|
42326
42853
|
|
|
42327
42854
|
const symbol = this._extractSymbol(dataItem);
|
|
42328
|
-
const messageType = this._extractMessageType(dataItem);
|
|
42329
42855
|
if (this.config.debug) {
|
|
42330
42856
|
console.log('[WebSocketManager] Processing data item:', {
|
|
42331
42857
|
symbol,
|
|
@@ -42485,25 +43011,15 @@ ${SharedStyles}
|
|
|
42485
43011
|
return symbolMappings[extracted] || extracted;
|
|
42486
43012
|
}
|
|
42487
43013
|
_extractMessageType(item) {
|
|
42488
|
-
//
|
|
42489
|
-
if (item._messageType) {
|
|
42490
|
-
return item._messageType;
|
|
42491
|
-
}
|
|
42492
|
-
|
|
42493
|
-
// Determine message type based on content
|
|
42494
|
-
if (item.type) return item.type;
|
|
42495
|
-
|
|
42496
|
-
// Infer type from data structure
|
|
43014
|
+
// FALLBACK: Infer type from data structure (for legacy/malformed messages)
|
|
42497
43015
|
|
|
42498
43016
|
// IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
|
|
42499
43017
|
// Level 2 ONBBO data - array of MMID objects or object with MMID field
|
|
42500
43018
|
if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
|
|
42501
43019
|
return 'queryonbbol2';
|
|
42502
43020
|
}
|
|
42503
|
-
|
|
42504
|
-
|
|
42505
|
-
if (item.MMID !== undefined) {
|
|
42506
|
-
return 'queryonbbol2';
|
|
43021
|
+
if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
|
|
43022
|
+
return 'queryoptionchain';
|
|
42507
43023
|
}
|
|
42508
43024
|
|
|
42509
43025
|
// Check for Source field AFTER MMID check
|