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.esm.js
CHANGED
|
@@ -406,6 +406,85 @@ class BaseWidget {
|
|
|
406
406
|
console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate initial symbol with the API and handle access/permission errors
|
|
412
|
+
* This is a reusable method for all widgets that need to validate symbols on initialization
|
|
413
|
+
*
|
|
414
|
+
* @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
|
|
415
|
+
* @param {string} symbol - The symbol to validate
|
|
416
|
+
* @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
|
|
417
|
+
* @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
|
|
418
|
+
*/
|
|
419
|
+
async validateInitialSymbol(apiMethod, symbol) {
|
|
420
|
+
let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
421
|
+
try {
|
|
422
|
+
const apiService = this.wsManager?.getApiService();
|
|
423
|
+
if (!apiService) {
|
|
424
|
+
if (this.debug) {
|
|
425
|
+
console.log(`[${this.constructor.name}] API service not available for initial validation`);
|
|
426
|
+
}
|
|
427
|
+
return true; // Continue without validation
|
|
428
|
+
}
|
|
429
|
+
if (!apiService[apiMethod]) {
|
|
430
|
+
console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
|
|
431
|
+
return true; // Continue without validation
|
|
432
|
+
}
|
|
433
|
+
const result = await apiService[apiMethod](symbol);
|
|
434
|
+
if (result && result.data && result.data.length > 0) {
|
|
435
|
+
if (this.debug) {
|
|
436
|
+
console.log(`[${this.constructor.name}] Initial symbol validated:`, {
|
|
437
|
+
symbol,
|
|
438
|
+
method: apiMethod,
|
|
439
|
+
dataCount: result.data.length
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Call success callback if provided
|
|
444
|
+
if (onSuccess && typeof onSuccess === 'function') {
|
|
445
|
+
onSuccess(result.data, result);
|
|
446
|
+
}
|
|
447
|
+
return true; // Validation successful
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// No data returned - might be access issue, but continue anyway
|
|
451
|
+
if (this.debug) {
|
|
452
|
+
console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
|
|
453
|
+
}
|
|
454
|
+
return true; // Continue anyway, WebSocket might still work
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (this.debug) {
|
|
457
|
+
console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check if it's an access/permission error
|
|
461
|
+
const errorMessage = (error.message || '').toLowerCase();
|
|
462
|
+
const isAccessError = errorMessage.includes('400') ||
|
|
463
|
+
// Bad Request (often used for access issues)
|
|
464
|
+
errorMessage.includes('401') ||
|
|
465
|
+
// Unauthorized
|
|
466
|
+
errorMessage.includes('403') ||
|
|
467
|
+
// Forbidden
|
|
468
|
+
errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
|
|
469
|
+
// Specific OPRA access denial
|
|
470
|
+
errorMessage.includes('permission denied') || errorMessage.includes('access denied');
|
|
471
|
+
if (isAccessError) {
|
|
472
|
+
// Hide loading and show access error
|
|
473
|
+
this.hideLoading();
|
|
474
|
+
|
|
475
|
+
// Extract more specific error message if available
|
|
476
|
+
let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
|
|
477
|
+
if (errorMessage.includes('no opra access')) {
|
|
478
|
+
userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
|
|
479
|
+
}
|
|
480
|
+
this.showError(userMessage);
|
|
481
|
+
return false; // Stop initialization
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// For other errors, continue with WebSocket (might still work)
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
409
488
|
}
|
|
410
489
|
|
|
411
490
|
// src/models/MarketDataModel.js
|
|
@@ -2504,28 +2583,6 @@ const TimeSalesStyles = `
|
|
|
2504
2583
|
font-size: 1.71em;
|
|
2505
2584
|
font-weight: 700;
|
|
2506
2585
|
color: #111827;
|
|
2507
|
-
cursor: pointer;
|
|
2508
|
-
transition: all 0.2s ease;
|
|
2509
|
-
position: relative;
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
.time-sales-widget .symbol.editable-symbol:hover {
|
|
2513
|
-
opacity: 0.8;
|
|
2514
|
-
transform: scale(1.02);
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
.time-sales-widget .symbol.editable-symbol:hover::after {
|
|
2518
|
-
content: "✎";
|
|
2519
|
-
position: absolute;
|
|
2520
|
-
top: -8px;
|
|
2521
|
-
right: -8px;
|
|
2522
|
-
background: #3b82f6;
|
|
2523
|
-
color: white;
|
|
2524
|
-
font-size: 10px;
|
|
2525
|
-
padding: 2px 4px;
|
|
2526
|
-
border-radius: 4px;
|
|
2527
|
-
opacity: 0.8;
|
|
2528
|
-
pointer-events: none;
|
|
2529
2586
|
}
|
|
2530
2587
|
|
|
2531
2588
|
/* ========================================
|
|
@@ -2800,8 +2857,36 @@ const TimeSalesStyles = `
|
|
|
2800
2857
|
}
|
|
2801
2858
|
`;
|
|
2802
2859
|
|
|
2860
|
+
/**
|
|
2861
|
+
* @deprecated SharedStyles is deprecated and will be removed in a future version.
|
|
2862
|
+
*
|
|
2863
|
+
* Please migrate to BaseStyles + CommonWidgetPatterns:
|
|
2864
|
+
*
|
|
2865
|
+
* BEFORE:
|
|
2866
|
+
* import { SharedStyles } from './styles/index.js';
|
|
2867
|
+
* export const MyWidgetStyles = `${SharedStyles} ...widget styles...`;
|
|
2868
|
+
*
|
|
2869
|
+
* AFTER:
|
|
2870
|
+
* import { BaseStyles } from './styles/BaseStyles.js';
|
|
2871
|
+
* import { getLoadingOverlayStyles, getErrorStyles } from './styles/CommonWidgetPatterns.js';
|
|
2872
|
+
* export const MyWidgetStyles = `
|
|
2873
|
+
* ${BaseStyles}
|
|
2874
|
+
* ${getLoadingOverlayStyles('my-widget')}
|
|
2875
|
+
* ${getErrorStyles('my-widget')}
|
|
2876
|
+
* ...widget-specific styles...
|
|
2877
|
+
* `;
|
|
2878
|
+
*
|
|
2879
|
+
* Benefits of migration:
|
|
2880
|
+
* - Proper CSS scoping (no global conflicts)
|
|
2881
|
+
* - Reduced code duplication
|
|
2882
|
+
* - Better maintainability
|
|
2883
|
+
* - Smaller bundle size
|
|
2884
|
+
*
|
|
2885
|
+
* See STYLING_CUSTOMIZATION_GUIDE.md for details.
|
|
2886
|
+
*/
|
|
2803
2887
|
const SharedStyles = `
|
|
2804
2888
|
/* ========================================
|
|
2889
|
+
⚠️ DEPRECATED - Use BaseStyles.js instead
|
|
2805
2890
|
FONT SIZE SYSTEM - CSS Variables
|
|
2806
2891
|
Adjust --mdas-base-font-size to scale all fonts
|
|
2807
2892
|
======================================== */
|
|
@@ -4477,99 +4562,46 @@ class MarketDataWidget extends BaseWidget {
|
|
|
4477
4562
|
this.showLoading();
|
|
4478
4563
|
|
|
4479
4564
|
// Validate initial symbol via API to get company info
|
|
4480
|
-
|
|
4481
|
-
this.
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
const apiService = this.wsManager.getApiService();
|
|
4486
|
-
if (!apiService) {
|
|
4487
|
-
if (this.debug) {
|
|
4488
|
-
console.log('[MarketDataWidget] API service not available for initial validation');
|
|
4489
|
-
}
|
|
4490
|
-
return;
|
|
4565
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
4566
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
4567
|
+
// Store for use in updateWidget
|
|
4568
|
+
if (data && data[0]) {
|
|
4569
|
+
this.initialValidationData = data[0];
|
|
4491
4570
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
if (this.debug) {
|
|
4498
|
-
console.log('[MarketDataWidget] Initial symbol validated:', {
|
|
4499
|
-
symbol: this.symbol,
|
|
4500
|
-
companyName: symbolData.comp_name,
|
|
4501
|
-
exchangeName: symbolData.market_name
|
|
4502
|
-
});
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
} catch (error) {
|
|
4506
|
-
if (this.debug) {
|
|
4507
|
-
console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
|
|
4508
|
-
}
|
|
4509
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
4571
|
+
});
|
|
4572
|
+
|
|
4573
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
4574
|
+
if (validationSuccess === false) {
|
|
4575
|
+
return; // Error is already shown, don't continue
|
|
4510
4576
|
}
|
|
4577
|
+
this.subscribeToData();
|
|
4511
4578
|
}
|
|
4512
4579
|
subscribeToData() {
|
|
4513
4580
|
// Subscribe with symbol for routing
|
|
4514
4581
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
|
|
4515
4582
|
);
|
|
4516
|
-
|
|
4517
|
-
// Send subscription message
|
|
4518
|
-
/* this.wsManager.send({
|
|
4519
|
-
type: 'queryl1',
|
|
4520
|
-
symbol: this.symbol
|
|
4521
|
-
}); */
|
|
4522
4583
|
}
|
|
4584
|
+
handleData(message) {
|
|
4585
|
+
//console.log('DEBUG', message)
|
|
4523
4586
|
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
if (this.debug) {
|
|
4529
|
-
console.log('[MarketDataWidget] Received:', event, data);
|
|
4530
|
-
}
|
|
4531
|
-
if (event === 'connection') {
|
|
4532
|
-
this.handleConnectionStatus(data);
|
|
4533
|
-
return;
|
|
4534
|
-
}
|
|
4535
|
-
if (event === 'data') {
|
|
4536
|
-
this.handleData(data);
|
|
4537
|
-
}
|
|
4538
|
-
if (event === 'session_revoked') {
|
|
4539
|
-
if (data.status === 'attempting_relogin') {
|
|
4540
|
-
this.showLoading();
|
|
4541
|
-
} else if (data.status === 'relogin_failed') {
|
|
4542
|
-
this.showError(data.error);
|
|
4543
|
-
} else if (data.status === 'relogin_successful') {
|
|
4544
|
-
this.hideLoading();
|
|
4545
|
-
}
|
|
4546
|
-
return;
|
|
4547
|
-
}
|
|
4548
|
-
} catch (error) {
|
|
4549
|
-
console.error('[MarketDataWidget] Error handling message:', error);
|
|
4550
|
-
this.showError('Error processing data');
|
|
4587
|
+
// Check for error: false and display message if present
|
|
4588
|
+
if (message.error === true) {
|
|
4589
|
+
if (this.debug) {
|
|
4590
|
+
console.log('[MarketDataWidget] Received error response:', message.message);
|
|
4551
4591
|
}
|
|
4552
|
-
|
|
4592
|
+
const errorMsg = message.message || message.Message;
|
|
4553
4593
|
|
|
4554
|
-
|
|
4555
|
-
if (
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
// Re-send subscription when reconnected
|
|
4560
|
-
this.wsManager.send({
|
|
4561
|
-
type: 'queryl1',
|
|
4562
|
-
symbol: this.symbol
|
|
4563
|
-
});
|
|
4564
|
-
} else if (status.status === 'disconnected') {
|
|
4565
|
-
this.showError('Disconnected from data service');
|
|
4566
|
-
} else if (status.status === 'error') {
|
|
4567
|
-
this.showError(status.error || 'Connection error');
|
|
4594
|
+
// If there's a message field, show it as an error
|
|
4595
|
+
if (errorMsg) {
|
|
4596
|
+
this.hideLoading();
|
|
4597
|
+
this.showError(errorMsg);
|
|
4598
|
+
return;
|
|
4568
4599
|
}
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4600
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
4601
|
+
this.hideLoading();
|
|
4602
|
+
this.showError(message.Message);
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4573
4605
|
if (this.loadingTimeout) {
|
|
4574
4606
|
clearTimeout(this.loadingTimeout);
|
|
4575
4607
|
this.loadingTimeout = null;
|
|
@@ -4655,126 +4687,6 @@ class MarketDataWidget extends BaseWidget {
|
|
|
4655
4687
|
this.showConnectionQuality(); // Show cache indicator
|
|
4656
4688
|
}
|
|
4657
4689
|
}
|
|
4658
|
-
showNoDataState() {
|
|
4659
|
-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
4660
|
-
if (this.isDestroyed) return;
|
|
4661
|
-
try {
|
|
4662
|
-
// Hide loading overlay
|
|
4663
|
-
this.hideLoading();
|
|
4664
|
-
|
|
4665
|
-
// Clear any existing errors
|
|
4666
|
-
this.clearError();
|
|
4667
|
-
|
|
4668
|
-
// Update header with dimmed styling and basic info from data
|
|
4669
|
-
const symbol = data.Symbol || this.symbol;
|
|
4670
|
-
|
|
4671
|
-
// Add null checks for all DOM elements
|
|
4672
|
-
const symbolElement = this.container.querySelector('.symbol');
|
|
4673
|
-
if (symbolElement) {
|
|
4674
|
-
symbolElement.textContent = symbol;
|
|
4675
|
-
}
|
|
4676
|
-
const companyNameElement = this.container.querySelector('.company-name');
|
|
4677
|
-
if (companyNameElement) {
|
|
4678
|
-
companyNameElement.textContent = `${symbol} Inc`;
|
|
4679
|
-
}
|
|
4680
|
-
|
|
4681
|
-
// Set price to $0.00 and add dimmed class
|
|
4682
|
-
const currentPriceElement = this.container.querySelector('.current-price');
|
|
4683
|
-
if (currentPriceElement) {
|
|
4684
|
-
currentPriceElement.textContent = '$0.00';
|
|
4685
|
-
}
|
|
4686
|
-
const changeElement = this.container.querySelector('.price-change');
|
|
4687
|
-
if (changeElement) {
|
|
4688
|
-
const changeValueElement = changeElement.querySelector('.change-value');
|
|
4689
|
-
const changePercentElement = changeElement.querySelector('.change-percent');
|
|
4690
|
-
if (changeValueElement) {
|
|
4691
|
-
changeValueElement.textContent = '+0.00';
|
|
4692
|
-
}
|
|
4693
|
-
if (changePercentElement) {
|
|
4694
|
-
changePercentElement.textContent = ' (0.00%)';
|
|
4695
|
-
}
|
|
4696
|
-
changeElement.classList.remove('positive', 'negative');
|
|
4697
|
-
changeElement.classList.add('neutral');
|
|
4698
|
-
}
|
|
4699
|
-
|
|
4700
|
-
// Add dimmed styling to header
|
|
4701
|
-
const widgetHeader = this.container.querySelector('.widget-header');
|
|
4702
|
-
if (widgetHeader) {
|
|
4703
|
-
widgetHeader.classList.add('dimmed');
|
|
4704
|
-
}
|
|
4705
|
-
|
|
4706
|
-
// Replace the data grid with no data message
|
|
4707
|
-
this.showNoDataMessage(symbol, data);
|
|
4708
|
-
|
|
4709
|
-
// Update footer with current timestamp
|
|
4710
|
-
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
4711
|
-
if (lastUpdateElement) {
|
|
4712
|
-
const timestamp = formatTimestampET();
|
|
4713
|
-
lastUpdateElement.textContent = `Checked: ${timestamp}`;
|
|
4714
|
-
}
|
|
4715
|
-
const dataSourceElement = this.container.querySelector('.data-source');
|
|
4716
|
-
if (dataSourceElement) {
|
|
4717
|
-
dataSourceElement.textContent = 'Source: No data available';
|
|
4718
|
-
}
|
|
4719
|
-
} catch (error) {
|
|
4720
|
-
console.error('Error showing no data state:', error);
|
|
4721
|
-
this.showError('Error displaying no data state');
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
showNoDataMessage(symbol) {
|
|
4725
|
-
const dataGrid = this.container.querySelector('.data-grid');
|
|
4726
|
-
|
|
4727
|
-
// Hide the data grid if it exists
|
|
4728
|
-
if (dataGrid) {
|
|
4729
|
-
dataGrid.style.display = 'none';
|
|
4730
|
-
}
|
|
4731
|
-
|
|
4732
|
-
// Remove existing no data message if present
|
|
4733
|
-
const existingNoData = this.container.querySelector('.no-data-state');
|
|
4734
|
-
if (existingNoData) {
|
|
4735
|
-
existingNoData.remove();
|
|
4736
|
-
}
|
|
4737
|
-
|
|
4738
|
-
// Create no data message element - safely without XSS risk
|
|
4739
|
-
const noDataElement = createElement('div', '', 'no-data-state');
|
|
4740
|
-
const noDataContent = createElement('div', '', 'no-data-content');
|
|
4741
|
-
|
|
4742
|
-
// Create icon
|
|
4743
|
-
const iconDiv = document.createElement('div');
|
|
4744
|
-
iconDiv.className = 'no-data-icon';
|
|
4745
|
-
iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
4746
|
-
<circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
|
|
4747
|
-
<path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
|
|
4748
|
-
<circle cx="12" cy="16" r="1" fill="#9ca3af"/>
|
|
4749
|
-
</svg>`;
|
|
4750
|
-
noDataContent.appendChild(iconDiv);
|
|
4751
|
-
|
|
4752
|
-
// Create title
|
|
4753
|
-
const title = createElement('h3', 'No Market Data', 'no-data-title');
|
|
4754
|
-
noDataContent.appendChild(title);
|
|
4755
|
-
|
|
4756
|
-
// Create description with sanitized symbol
|
|
4757
|
-
const description = createElement('p', '', 'no-data-description');
|
|
4758
|
-
description.appendChild(document.createTextNode('Market data for '));
|
|
4759
|
-
const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
|
|
4760
|
-
description.appendChild(symbolStrong);
|
|
4761
|
-
description.appendChild(document.createTextNode(' was not found'));
|
|
4762
|
-
noDataContent.appendChild(description);
|
|
4763
|
-
|
|
4764
|
-
// Create guidance
|
|
4765
|
-
const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
|
|
4766
|
-
noDataContent.appendChild(guidance);
|
|
4767
|
-
noDataElement.appendChild(noDataContent);
|
|
4768
|
-
|
|
4769
|
-
// Insert before footer, with null check
|
|
4770
|
-
const footer = this.container.querySelector('.widget-footer');
|
|
4771
|
-
if (footer && footer.parentNode) {
|
|
4772
|
-
footer.parentNode.insertBefore(noDataElement, footer);
|
|
4773
|
-
} else {
|
|
4774
|
-
// Fallback: append to container if footer not found
|
|
4775
|
-
this.container.appendChild(noDataElement);
|
|
4776
|
-
}
|
|
4777
|
-
}
|
|
4778
4690
|
updateWidget(data) {
|
|
4779
4691
|
if (this.isDestroyed) return;
|
|
4780
4692
|
try {
|
|
@@ -5279,7 +5191,20 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5279
5191
|
this.showLoading();
|
|
5280
5192
|
|
|
5281
5193
|
// Validate initial symbol via API to get company info
|
|
5282
|
-
|
|
5194
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
5195
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
5196
|
+
// Extract company info from first data item
|
|
5197
|
+
if (data && data[0]) {
|
|
5198
|
+
this.companyName = data[0].comp_name || '';
|
|
5199
|
+
this.exchangeName = data[0].market_name || '';
|
|
5200
|
+
this.mic = data[0].mic || '';
|
|
5201
|
+
}
|
|
5202
|
+
});
|
|
5203
|
+
|
|
5204
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
5205
|
+
if (validationSuccess === false) {
|
|
5206
|
+
return; // Error is already shown, don't continue
|
|
5207
|
+
}
|
|
5283
5208
|
|
|
5284
5209
|
// Set timeout to detect no data on initial load
|
|
5285
5210
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -5296,38 +5221,6 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5296
5221
|
|
|
5297
5222
|
this.subscribeToData();
|
|
5298
5223
|
}
|
|
5299
|
-
async validateInitialSymbol() {
|
|
5300
|
-
try {
|
|
5301
|
-
const apiService = this.wsManager.getApiService();
|
|
5302
|
-
if (!apiService) {
|
|
5303
|
-
if (this.debug) {
|
|
5304
|
-
console.log('[NightSessionWidget] API service not available for initial validation');
|
|
5305
|
-
}
|
|
5306
|
-
return;
|
|
5307
|
-
}
|
|
5308
|
-
const result = await apiService.quotel1(this.symbol);
|
|
5309
|
-
if (result && result.data && result.data[0]) {
|
|
5310
|
-
const symbolData = result.data[0];
|
|
5311
|
-
// Extract company info
|
|
5312
|
-
this.companyName = symbolData.comp_name || '';
|
|
5313
|
-
this.exchangeName = symbolData.market_name || '';
|
|
5314
|
-
this.mic = symbolData.mic || '';
|
|
5315
|
-
if (this.debug) {
|
|
5316
|
-
console.log('[NightSessionWidget] Initial symbol validated:', {
|
|
5317
|
-
symbol: this.symbol,
|
|
5318
|
-
companyName: this.companyName,
|
|
5319
|
-
exchangeName: this.exchangeName,
|
|
5320
|
-
mic: this.mic
|
|
5321
|
-
});
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
} catch (error) {
|
|
5325
|
-
if (this.debug) {
|
|
5326
|
-
console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
|
|
5327
|
-
}
|
|
5328
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
5329
|
-
}
|
|
5330
|
-
}
|
|
5331
5224
|
subscribeToData() {
|
|
5332
5225
|
let subscriptionType;
|
|
5333
5226
|
if (this.source === 'bruce') {
|
|
@@ -5347,6 +5240,7 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5347
5240
|
}
|
|
5348
5241
|
handleData(message) {
|
|
5349
5242
|
console.log('DEBUG NIGHT', message);
|
|
5243
|
+
|
|
5350
5244
|
//message = message.data || message.Data
|
|
5351
5245
|
|
|
5352
5246
|
if (this.loadingTimeout) {
|
|
@@ -5354,64 +5248,41 @@ class NightSessionWidget extends BaseWidget {
|
|
|
5354
5248
|
this.loadingTimeout = null;
|
|
5355
5249
|
}
|
|
5356
5250
|
|
|
5357
|
-
//
|
|
5358
|
-
if (message.
|
|
5251
|
+
// Check for error: false and display message if present
|
|
5252
|
+
if (message.error === true) {
|
|
5359
5253
|
if (this.debug) {
|
|
5360
|
-
console.log('[
|
|
5254
|
+
console.log('[Nigh] Received error response:', message.message);
|
|
5361
5255
|
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
NotFound: true,
|
|
5367
|
-
message: message.message
|
|
5368
|
-
});
|
|
5369
|
-
} else {
|
|
5256
|
+
const errorMsg = message.message || message.Message;
|
|
5257
|
+
|
|
5258
|
+
// If there's a message field, show it as an error
|
|
5259
|
+
if (errorMsg) {
|
|
5370
5260
|
this.hideLoading();
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
}
|
|
5261
|
+
this.showError(errorMsg);
|
|
5262
|
+
return;
|
|
5374
5263
|
}
|
|
5264
|
+
} else if (message.Data && typeof message.Data === 'string') {
|
|
5265
|
+
this.hideLoading();
|
|
5266
|
+
this.showError(message.Message);
|
|
5375
5267
|
return;
|
|
5376
5268
|
}
|
|
5377
5269
|
|
|
5378
|
-
// Handle
|
|
5270
|
+
// Handle error messages from server (plain text converted to structured format)
|
|
5379
5271
|
if (message.type === 'error') {
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
this.showNoDataState({
|
|
5387
|
-
Symbol: this.symbol,
|
|
5388
|
-
NotFound: true,
|
|
5389
|
-
isAccessError: true,
|
|
5390
|
-
message: errorMsg
|
|
5391
|
-
});
|
|
5392
|
-
} else {
|
|
5393
|
-
this.hideLoading();
|
|
5394
|
-
if (this.debug) {
|
|
5395
|
-
console.log('[NightSessionWidget] Access error but keeping cached data');
|
|
5396
|
-
}
|
|
5397
|
-
}
|
|
5272
|
+
if (this.debug) {
|
|
5273
|
+
console.log('[NightSessionWidget] Received no data message:', message.message);
|
|
5274
|
+
}
|
|
5275
|
+
// Only show no data state if we don't have cached data
|
|
5276
|
+
if (!this.data) {
|
|
5277
|
+
this.showError(errorMsg);
|
|
5398
5278
|
} else {
|
|
5399
|
-
|
|
5400
|
-
if (
|
|
5401
|
-
console.log('
|
|
5402
|
-
this.showError(errorMsg);
|
|
5403
|
-
} else {
|
|
5404
|
-
this.hideLoading();
|
|
5405
|
-
if (this.debug) {
|
|
5406
|
-
console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
|
|
5407
|
-
}
|
|
5279
|
+
this.hideLoading();
|
|
5280
|
+
if (this.debug) {
|
|
5281
|
+
console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
|
|
5408
5282
|
}
|
|
5409
5283
|
}
|
|
5410
5284
|
return;
|
|
5411
5285
|
}
|
|
5412
|
-
|
|
5413
|
-
// Filter for night session data
|
|
5414
|
-
|
|
5415
5286
|
if (Array.isArray(message.data)) {
|
|
5416
5287
|
// First, try to find data matching our symbol regardless of MarketName
|
|
5417
5288
|
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
@@ -6312,36 +6183,20 @@ class OptionsWidget extends BaseWidget {
|
|
|
6312
6183
|
this.showLoading();
|
|
6313
6184
|
|
|
6314
6185
|
// Validate initial symbol via API to get symbol info
|
|
6315
|
-
|
|
6316
|
-
this.
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
if (!apiService) {
|
|
6322
|
-
if (this.debug) {
|
|
6323
|
-
console.log('[OptionsWidget] API service not available for initial validation');
|
|
6324
|
-
}
|
|
6325
|
-
return;
|
|
6326
|
-
}
|
|
6327
|
-
const result = await apiService.quoteOptionl1(this.symbol);
|
|
6328
|
-
if (result && result[0] && !result[0].error && !result[0].not_found) {
|
|
6329
|
-
const symbolData = result[0];
|
|
6330
|
-
// Store for use in updateWidget
|
|
6331
|
-
this.initialValidationData = symbolData;
|
|
6332
|
-
if (this.debug) {
|
|
6333
|
-
console.log('[OptionsWidget] Initial symbol validated:', {
|
|
6334
|
-
symbol: this.symbol,
|
|
6335
|
-
underlying: symbolData.Underlying || symbolData.RootSymbol
|
|
6336
|
-
});
|
|
6337
|
-
}
|
|
6186
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting data
|
|
6187
|
+
const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
|
|
6188
|
+
// Store for use in updateWidget
|
|
6189
|
+
// Note: quoteOptionl1 returns array directly, not wrapped in data field
|
|
6190
|
+
if (data && data[0] && !data[0].error && !data[0].not_found) {
|
|
6191
|
+
this.initialValidationData = data[0];
|
|
6338
6192
|
}
|
|
6339
|
-
}
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
//
|
|
6193
|
+
});
|
|
6194
|
+
|
|
6195
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
6196
|
+
if (validationSuccess === false) {
|
|
6197
|
+
return; // Error is already shown, don't continue
|
|
6344
6198
|
}
|
|
6199
|
+
this.subscribeToData();
|
|
6345
6200
|
}
|
|
6346
6201
|
subscribeToData() {
|
|
6347
6202
|
// Subscribe with symbol for routing
|
|
@@ -6811,6 +6666,15 @@ const OptionChainTemplate = `
|
|
|
6811
6666
|
</select>
|
|
6812
6667
|
<button class="fetch-button" disabled>Search</button>
|
|
6813
6668
|
</div>
|
|
6669
|
+
<div class="filter-section">
|
|
6670
|
+
<label for="strike-filter" class="filter-label">Display:</label>
|
|
6671
|
+
<select class="strike-filter" id="strike-filter">
|
|
6672
|
+
<option value="5">Near the money (±5 strikes)</option>
|
|
6673
|
+
<option value="10">Near the money (±10 strikes)</option>
|
|
6674
|
+
<option value="15">Near the money (±15 strikes)</option>
|
|
6675
|
+
<option value="all">All strikes</option>
|
|
6676
|
+
</select>
|
|
6677
|
+
</div>
|
|
6814
6678
|
</div>
|
|
6815
6679
|
|
|
6816
6680
|
<!-- Data Grid Section -->
|
|
@@ -6867,11 +6731,383 @@ const OptionChainTemplate = `
|
|
|
6867
6731
|
</div>
|
|
6868
6732
|
`;
|
|
6869
6733
|
|
|
6734
|
+
// src/widgets/styles/BaseStyles.js
|
|
6735
|
+
|
|
6736
|
+
/**
|
|
6737
|
+
* Base Styles - CSS Variables and Universal Utilities
|
|
6738
|
+
*
|
|
6739
|
+
* This file contains:
|
|
6740
|
+
* 1. CSS Custom Properties (variables) for consistent theming
|
|
6741
|
+
* 2. Responsive font sizing system
|
|
6742
|
+
* 3. Optional utility classes
|
|
6743
|
+
*
|
|
6744
|
+
* Usage:
|
|
6745
|
+
* import { BaseStyles } from './styles/BaseStyles';
|
|
6746
|
+
* export const MyWidgetStyles = `${BaseStyles} ...widget styles...`;
|
|
6747
|
+
*/
|
|
6748
|
+
|
|
6749
|
+
const BaseStyles = `
|
|
6750
|
+
/* ============================================
|
|
6751
|
+
MDAS WIDGET CSS VARIABLES
|
|
6752
|
+
============================================ */
|
|
6753
|
+
|
|
6754
|
+
:root {
|
|
6755
|
+
/* --- FONT SIZE SYSTEM --- */
|
|
6756
|
+
/* Base font size - all other sizes are relative to this */
|
|
6757
|
+
--mdas-base-font-size: 14px;
|
|
6758
|
+
|
|
6759
|
+
/* Component-specific font sizes (em units scale with base) */
|
|
6760
|
+
--mdas-small-text-size: 0.79em; /* 11px at 14px base */
|
|
6761
|
+
--mdas-medium-text-size: 0.93em; /* 13px at 14px base */
|
|
6762
|
+
--mdas-large-text-size: 1.14em; /* 16px at 14px base */
|
|
6763
|
+
|
|
6764
|
+
/* Widget element sizes */
|
|
6765
|
+
--mdas-company-name-size: 1.43em; /* 20px at 14px base */
|
|
6766
|
+
--mdas-symbol-size: 1.79em; /* 25px at 14px base */
|
|
6767
|
+
--mdas-price-size: 2.29em; /* 32px at 14px base */
|
|
6768
|
+
--mdas-change-size: 1.14em; /* 16px at 14px base */
|
|
6769
|
+
|
|
6770
|
+
/* Data display sizes */
|
|
6771
|
+
--mdas-label-size: 0.86em; /* 12px at 14px base */
|
|
6772
|
+
--mdas-value-size: 1em; /* 14px at 14px base */
|
|
6773
|
+
--mdas-footer-size: 0.79em; /* 11px at 14px base */
|
|
6774
|
+
|
|
6775
|
+
/* Chart-specific sizes */
|
|
6776
|
+
--mdas-chart-title-size: 1.29em; /* 18px at 14px base */
|
|
6777
|
+
--mdas-chart-label-size: 0.93em; /* 13px at 14px base */
|
|
6778
|
+
--mdas-chart-value-size: 1.43em; /* 20px at 14px base */
|
|
6779
|
+
|
|
6780
|
+
/* --- COLOR PALETTE (for future theming support) --- */
|
|
6781
|
+
/* Primary colors */
|
|
6782
|
+
--mdas-primary-color: #3b82f6;
|
|
6783
|
+
--mdas-primary-hover: #2563eb;
|
|
6784
|
+
|
|
6785
|
+
/* Status colors */
|
|
6786
|
+
--mdas-color-positive: #059669;
|
|
6787
|
+
--mdas-color-positive-bg: #d1fae5;
|
|
6788
|
+
--mdas-color-negative: #dc2626;
|
|
6789
|
+
--mdas-color-negative-bg: #fee2e2;
|
|
6790
|
+
--mdas-color-neutral: #6b7280;
|
|
6791
|
+
|
|
6792
|
+
/* Background colors */
|
|
6793
|
+
--mdas-bg-primary: #ffffff;
|
|
6794
|
+
--mdas-bg-secondary: #f9fafb;
|
|
6795
|
+
--mdas-bg-tertiary: #f3f4f6;
|
|
6796
|
+
|
|
6797
|
+
/* Text colors */
|
|
6798
|
+
--mdas-text-primary: #111827;
|
|
6799
|
+
--mdas-text-secondary: #6b7280;
|
|
6800
|
+
--mdas-text-tertiary: #9ca3af;
|
|
6801
|
+
|
|
6802
|
+
/* Border colors */
|
|
6803
|
+
--mdas-border-primary: #e5e7eb;
|
|
6804
|
+
--mdas-border-secondary: #d1d5db;
|
|
6805
|
+
|
|
6806
|
+
/* --- SPACING SYSTEM --- */
|
|
6807
|
+
--mdas-spacing-xs: 4px;
|
|
6808
|
+
--mdas-spacing-sm: 8px;
|
|
6809
|
+
--mdas-spacing-md: 16px;
|
|
6810
|
+
--mdas-spacing-lg: 24px;
|
|
6811
|
+
--mdas-spacing-xl: 32px;
|
|
6812
|
+
|
|
6813
|
+
/* --- EFFECTS --- */
|
|
6814
|
+
--mdas-border-radius: 8px;
|
|
6815
|
+
--mdas-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
6816
|
+
--mdas-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
6817
|
+
--mdas-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
|
6818
|
+
|
|
6819
|
+
/* --- Z-INDEX LAYERS --- */
|
|
6820
|
+
--mdas-z-base: 1;
|
|
6821
|
+
--mdas-z-sticky: 10;
|
|
6822
|
+
--mdas-z-overlay: 100;
|
|
6823
|
+
--mdas-z-modal: 10000;
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
/* ============================================
|
|
6827
|
+
RESPONSIVE FONT SCALING
|
|
6828
|
+
============================================ */
|
|
6829
|
+
|
|
6830
|
+
/* Tablet breakpoint - slightly smaller fonts */
|
|
6831
|
+
@media (max-width: 768px) {
|
|
6832
|
+
:root {
|
|
6833
|
+
--mdas-base-font-size: 13px;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
/* Mobile breakpoint - smaller fonts for small screens */
|
|
6838
|
+
@media (max-width: 480px) {
|
|
6839
|
+
:root {
|
|
6840
|
+
--mdas-base-font-size: 12px;
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
|
|
6844
|
+
/* ============================================
|
|
6845
|
+
OPTIONAL UTILITY CLASSES
|
|
6846
|
+
Use these classes for consistent styling
|
|
6847
|
+
============================================ */
|
|
6848
|
+
|
|
6849
|
+
/* Widget card styling - apply to widget root element */
|
|
6850
|
+
.mdas-card {
|
|
6851
|
+
background: var(--mdas-bg-primary);
|
|
6852
|
+
border-radius: var(--mdas-border-radius);
|
|
6853
|
+
padding: var(--mdas-spacing-lg);
|
|
6854
|
+
box-shadow: var(--mdas-shadow-md);
|
|
6855
|
+
border: 1px solid var(--mdas-border-primary);
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
/* Responsive container */
|
|
6859
|
+
.mdas-container {
|
|
6860
|
+
width: 100%;
|
|
6861
|
+
max-width: 1400px;
|
|
6862
|
+
margin: 0 auto;
|
|
6863
|
+
}
|
|
6864
|
+
|
|
6865
|
+
/* Text utilities */
|
|
6866
|
+
.mdas-text-primary {
|
|
6867
|
+
color: var(--mdas-text-primary);
|
|
6868
|
+
}
|
|
6869
|
+
|
|
6870
|
+
.mdas-text-secondary {
|
|
6871
|
+
color: var(--mdas-text-secondary);
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
.mdas-text-muted {
|
|
6875
|
+
color: var(--mdas-text-tertiary);
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
/* Status colors */
|
|
6879
|
+
.mdas-positive {
|
|
6880
|
+
color: var(--mdas-color-positive);
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
.mdas-negative {
|
|
6884
|
+
color: var(--mdas-color-negative);
|
|
6885
|
+
}
|
|
6886
|
+
|
|
6887
|
+
.mdas-neutral {
|
|
6888
|
+
color: var(--mdas-color-neutral);
|
|
6889
|
+
}
|
|
6890
|
+
`;
|
|
6891
|
+
|
|
6892
|
+
// src/widgets/styles/CommonWidgetPatterns.js
|
|
6893
|
+
|
|
6894
|
+
/**
|
|
6895
|
+
* Common Widget Patterns - Reusable Style Functions
|
|
6896
|
+
*
|
|
6897
|
+
* These functions generate properly scoped CSS for common widget patterns.
|
|
6898
|
+
* Each function takes a widget class name and returns scoped CSS.
|
|
6899
|
+
*
|
|
6900
|
+
* Benefits:
|
|
6901
|
+
* - Eliminates code duplication
|
|
6902
|
+
* - Ensures proper scoping (no global conflicts)
|
|
6903
|
+
* - Consistent styling across widgets
|
|
6904
|
+
* - Easy to maintain and update
|
|
6905
|
+
*
|
|
6906
|
+
* Usage:
|
|
6907
|
+
* import { getLoadingOverlayStyles } from './CommonWidgetPatterns';
|
|
6908
|
+
* const styles = `${getLoadingOverlayStyles('my-widget')} ...other styles...`;
|
|
6909
|
+
*/
|
|
6910
|
+
|
|
6911
|
+
/**
|
|
6912
|
+
* Generate loading overlay styles for a widget
|
|
6913
|
+
* @param {string} widgetClass - Widget class name (e.g., 'option-chain-widget')
|
|
6914
|
+
* @returns {string} Scoped CSS for loading overlay
|
|
6915
|
+
*/
|
|
6916
|
+
const getLoadingOverlayStyles = widgetClass => `
|
|
6917
|
+
/* Loading Overlay for ${widgetClass} */
|
|
6918
|
+
.${widgetClass} .widget-loading-overlay {
|
|
6919
|
+
position: absolute;
|
|
6920
|
+
top: 0;
|
|
6921
|
+
left: 0;
|
|
6922
|
+
right: 0;
|
|
6923
|
+
bottom: 0;
|
|
6924
|
+
background: rgba(255, 255, 255, 0.95);
|
|
6925
|
+
backdrop-filter: blur(2px);
|
|
6926
|
+
display: flex;
|
|
6927
|
+
flex-direction: column;
|
|
6928
|
+
align-items: center;
|
|
6929
|
+
justify-content: center;
|
|
6930
|
+
z-index: var(--mdas-z-overlay, 100);
|
|
6931
|
+
border-radius: var(--mdas-border-radius, 12px);
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
.${widgetClass} .widget-loading-overlay.hidden {
|
|
6935
|
+
display: none;
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6938
|
+
.${widgetClass} .loading-content {
|
|
6939
|
+
display: flex;
|
|
6940
|
+
flex-direction: column;
|
|
6941
|
+
align-items: center;
|
|
6942
|
+
gap: 12px;
|
|
6943
|
+
}
|
|
6944
|
+
|
|
6945
|
+
.${widgetClass} .loading-spinner {
|
|
6946
|
+
width: 40px;
|
|
6947
|
+
height: 40px;
|
|
6948
|
+
border: 4px solid var(--mdas-border-primary, #e5e7eb);
|
|
6949
|
+
border-top-color: var(--mdas-primary-color, #3b82f6);
|
|
6950
|
+
border-radius: 50%;
|
|
6951
|
+
animation: ${widgetClass}-spin 1s linear infinite;
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
.${widgetClass} .loading-text {
|
|
6955
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
6956
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
6957
|
+
font-weight: 500;
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
@keyframes ${widgetClass}-spin {
|
|
6961
|
+
to { transform: rotate(360deg); }
|
|
6962
|
+
}
|
|
6963
|
+
`;
|
|
6964
|
+
|
|
6965
|
+
/**
|
|
6966
|
+
* Generate widget error display styles
|
|
6967
|
+
* @param {string} widgetClass - Widget class name
|
|
6968
|
+
* @returns {string} Scoped CSS for error display
|
|
6969
|
+
*/
|
|
6970
|
+
const getErrorStyles = widgetClass => `
|
|
6971
|
+
/* Error Display for ${widgetClass} */
|
|
6972
|
+
.${widgetClass} .widget-error {
|
|
6973
|
+
padding: 16px 20px;
|
|
6974
|
+
background: var(--mdas-color-negative-bg, #fee2e2);
|
|
6975
|
+
border: 1px solid #fecaca;
|
|
6976
|
+
border-radius: var(--mdas-border-radius, 8px);
|
|
6977
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
6978
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
6979
|
+
margin: 16px;
|
|
6980
|
+
text-align: center;
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
.${widgetClass} .widget-error-container {
|
|
6984
|
+
display: flex;
|
|
6985
|
+
flex-direction: column;
|
|
6986
|
+
align-items: center;
|
|
6987
|
+
gap: 12px;
|
|
6988
|
+
padding: 24px;
|
|
6989
|
+
}
|
|
6990
|
+
`;
|
|
6991
|
+
|
|
6992
|
+
/**
|
|
6993
|
+
* Generate widget footer styles
|
|
6994
|
+
* @param {string} widgetClass - Widget class name
|
|
6995
|
+
* @returns {string} Scoped CSS for widget footer
|
|
6996
|
+
*/
|
|
6997
|
+
const getFooterStyles = widgetClass => `
|
|
6998
|
+
/* Footer for ${widgetClass} */
|
|
6999
|
+
.${widgetClass} .widget-footer {
|
|
7000
|
+
display: flex;
|
|
7001
|
+
justify-content: space-between;
|
|
7002
|
+
align-items: center;
|
|
7003
|
+
padding: 8px 16px;
|
|
7004
|
+
background: var(--mdas-bg-secondary, #f9fafb);
|
|
7005
|
+
border-top: 1px solid var(--mdas-border-primary, #e5e7eb);
|
|
7006
|
+
font-size: var(--mdas-footer-size, 0.79em);
|
|
7007
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7008
|
+
}
|
|
7009
|
+
|
|
7010
|
+
.${widgetClass} .widget-footer .last-update {
|
|
7011
|
+
color: var(--mdas-text-tertiary, #9ca3af);
|
|
7012
|
+
}
|
|
7013
|
+
|
|
7014
|
+
.${widgetClass} .widget-footer .data-source {
|
|
7015
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7016
|
+
font-weight: 500;
|
|
7017
|
+
}
|
|
7018
|
+
`;
|
|
7019
|
+
|
|
7020
|
+
/**
|
|
7021
|
+
* Generate no-data state styles
|
|
7022
|
+
* @param {string} widgetClass - Widget class name
|
|
7023
|
+
* @returns {string} Scoped CSS for no-data state
|
|
7024
|
+
*/
|
|
7025
|
+
const getNoDataStyles = widgetClass => `
|
|
7026
|
+
/* No Data State for ${widgetClass} */
|
|
7027
|
+
.${widgetClass} .no-data-state {
|
|
7028
|
+
display: flex;
|
|
7029
|
+
flex-direction: column;
|
|
7030
|
+
align-items: center;
|
|
7031
|
+
justify-content: center;
|
|
7032
|
+
padding: 40px 20px;
|
|
7033
|
+
text-align: center;
|
|
7034
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
.${widgetClass} .no-data-content {
|
|
7038
|
+
max-width: 400px;
|
|
7039
|
+
display: flex;
|
|
7040
|
+
flex-direction: column;
|
|
7041
|
+
align-items: center;
|
|
7042
|
+
gap: 16px;
|
|
7043
|
+
}
|
|
7044
|
+
|
|
7045
|
+
.${widgetClass} .no-data-icon {
|
|
7046
|
+
width: 64px;
|
|
7047
|
+
height: 64px;
|
|
7048
|
+
display: flex;
|
|
7049
|
+
align-items: center;
|
|
7050
|
+
justify-content: center;
|
|
7051
|
+
opacity: 0.5;
|
|
7052
|
+
}
|
|
7053
|
+
|
|
7054
|
+
.${widgetClass} .no-data-icon svg {
|
|
7055
|
+
width: 100%;
|
|
7056
|
+
height: 100%;
|
|
7057
|
+
}
|
|
7058
|
+
|
|
7059
|
+
.${widgetClass} .no-data-title {
|
|
7060
|
+
font-size: var(--mdas-large-text-size, 1.14em);
|
|
7061
|
+
font-weight: 600;
|
|
7062
|
+
color: var(--mdas-text-primary, #111827);
|
|
7063
|
+
margin: 0;
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
.${widgetClass} .no-data-description {
|
|
7067
|
+
font-size: var(--mdas-medium-text-size, 0.93em);
|
|
7068
|
+
color: var(--mdas-text-secondary, #6b7280);
|
|
7069
|
+
margin: 0;
|
|
7070
|
+
line-height: 1.5;
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
.${widgetClass} .no-data-description strong {
|
|
7074
|
+
color: var(--mdas-text-primary, #111827);
|
|
7075
|
+
font-weight: 600;
|
|
7076
|
+
}
|
|
7077
|
+
|
|
7078
|
+
.${widgetClass} .no-data-guidance {
|
|
7079
|
+
font-size: var(--mdas-small-text-size, 0.79em);
|
|
7080
|
+
color: var(--mdas-text-tertiary, #9ca3af);
|
|
7081
|
+
margin: 0;
|
|
7082
|
+
}
|
|
7083
|
+
|
|
7084
|
+
/* Error variant of no-data state */
|
|
7085
|
+
.${widgetClass} .no-data-state.error-access {
|
|
7086
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
.${widgetClass} .no-data-state.error-access .no-data-title {
|
|
7090
|
+
color: var(--mdas-color-negative, #dc2626);
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
.${widgetClass} .no-data-state.error-access .no-data-icon {
|
|
7094
|
+
opacity: 0.8;
|
|
7095
|
+
}
|
|
7096
|
+
|
|
7097
|
+
.${widgetClass} .no-data-state.error-access .no-data-icon svg circle[fill] {
|
|
7098
|
+
fill: var(--mdas-color-negative, #dc2626);
|
|
7099
|
+
}
|
|
7100
|
+
`;
|
|
7101
|
+
|
|
6870
7102
|
// src/widgets/styles/OptionChainStyles.js
|
|
6871
7103
|
const OptionChainStyles = `
|
|
6872
|
-
${
|
|
7104
|
+
${BaseStyles}
|
|
7105
|
+
${getLoadingOverlayStyles('option-chain-widget')}
|
|
7106
|
+
${getErrorStyles('option-chain-widget')}
|
|
7107
|
+
${getFooterStyles('option-chain-widget')}
|
|
7108
|
+
${getNoDataStyles('option-chain-widget')}
|
|
6873
7109
|
|
|
6874
|
-
/*
|
|
7110
|
+
/* Option Chain Widget Specific Styles */
|
|
6875
7111
|
.option-chain-widget {
|
|
6876
7112
|
border: 1px solid #e5e7eb;
|
|
6877
7113
|
border-radius: 8px;
|
|
@@ -6896,8 +7132,22 @@ ${SharedStyles}
|
|
|
6896
7132
|
flex-wrap: wrap;
|
|
6897
7133
|
}
|
|
6898
7134
|
|
|
7135
|
+
.option-chain-widget .filter-section {
|
|
7136
|
+
display: flex;
|
|
7137
|
+
gap: 8px;
|
|
7138
|
+
align-items: center;
|
|
7139
|
+
margin-top: 8px;
|
|
7140
|
+
}
|
|
7141
|
+
|
|
7142
|
+
.option-chain-widget .filter-label {
|
|
7143
|
+
font-size: 13px;
|
|
7144
|
+
font-weight: 500;
|
|
7145
|
+
color: #374151;
|
|
7146
|
+
}
|
|
7147
|
+
|
|
6899
7148
|
.option-chain-widget .symbol-input,
|
|
6900
|
-
.option-chain-widget .date-select
|
|
7149
|
+
.option-chain-widget .date-select,
|
|
7150
|
+
.option-chain-widget .strike-filter {
|
|
6901
7151
|
padding: 8px 12px;
|
|
6902
7152
|
border: 1px solid #d1d5db;
|
|
6903
7153
|
border-radius: 4px;
|
|
@@ -6918,6 +7168,12 @@ ${SharedStyles}
|
|
|
6918
7168
|
background: white;
|
|
6919
7169
|
}
|
|
6920
7170
|
|
|
7171
|
+
.option-chain-widget .strike-filter {
|
|
7172
|
+
min-width: 200px;
|
|
7173
|
+
background: white;
|
|
7174
|
+
cursor: pointer;
|
|
7175
|
+
}
|
|
7176
|
+
|
|
6921
7177
|
.option-chain-widget .fetch-button {
|
|
6922
7178
|
padding: 8px 16px;
|
|
6923
7179
|
background: #3b82f6;
|
|
@@ -7073,10 +7329,22 @@ ${SharedStyles}
|
|
|
7073
7329
|
min-height: 32px;
|
|
7074
7330
|
}
|
|
7075
7331
|
|
|
7332
|
+
/* In-the-money highlighting */
|
|
7333
|
+
.option-chain-widget .calls-data.in-the-money span,
|
|
7334
|
+
.option-chain-widget .puts-data.in-the-money span {
|
|
7335
|
+
background-color: #fffbeb;
|
|
7336
|
+
}
|
|
7337
|
+
|
|
7076
7338
|
.option-chain-widget .option-row:hover {
|
|
7077
7339
|
background: #f9fafb;
|
|
7078
7340
|
}
|
|
7079
|
-
|
|
7341
|
+
|
|
7342
|
+
.option-chain-widget .option-row:hover .calls-data.in-the-money span,
|
|
7343
|
+
.option-chain-widget .option-row:hover .puts-data.in-the-money span {
|
|
7344
|
+
background-color: #fef3c7;
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
.option-chain-widget .option-row:hover .calls-data span,
|
|
7080
7348
|
.option-chain-widget .option-row:hover .puts-data span,
|
|
7081
7349
|
.option-chain-widget .option-row:hover .strike-data {
|
|
7082
7350
|
background: #f9fafb;
|
|
@@ -7100,14 +7368,32 @@ ${SharedStyles}
|
|
|
7100
7368
|
.option-chain-widget .puts-data span {
|
|
7101
7369
|
padding: 6px 4px;
|
|
7102
7370
|
text-align: center;
|
|
7103
|
-
font-size: 12px;
|
|
7104
|
-
font-weight: 400;
|
|
7105
|
-
color: #111827;
|
|
7371
|
+
font-size: 12px;
|
|
7372
|
+
font-weight: 400;
|
|
7373
|
+
color: #111827;
|
|
7106
7374
|
overflow: hidden;
|
|
7107
7375
|
text-overflow: ellipsis;
|
|
7108
7376
|
white-space: nowrap;
|
|
7109
7377
|
}
|
|
7110
7378
|
|
|
7379
|
+
/* Override color for change values with positive/negative classes */
|
|
7380
|
+
.option-chain-widget .calls-data span.positive,
|
|
7381
|
+
.option-chain-widget .puts-data span.positive {
|
|
7382
|
+
color: #059669 !important;
|
|
7383
|
+
font-weight: 500;
|
|
7384
|
+
}
|
|
7385
|
+
|
|
7386
|
+
.option-chain-widget .calls-data span.negative,
|
|
7387
|
+
.option-chain-widget .puts-data span.negative {
|
|
7388
|
+
color: #dc2626 !important;
|
|
7389
|
+
font-weight: 500;
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
.option-chain-widget .calls-data span.neutral,
|
|
7393
|
+
.option-chain-widget .puts-data span.neutral {
|
|
7394
|
+
color: #6b7280 !important;
|
|
7395
|
+
}
|
|
7396
|
+
|
|
7111
7397
|
.option-chain-widget .contract-cell {
|
|
7112
7398
|
font-size: 10px;
|
|
7113
7399
|
color: #6b7280;
|
|
@@ -7119,78 +7405,42 @@ ${SharedStyles}
|
|
|
7119
7405
|
white-space: nowrap;
|
|
7120
7406
|
}
|
|
7121
7407
|
|
|
7122
|
-
.option-chain-widget .
|
|
7123
|
-
color: #
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
color: #dc2626;
|
|
7128
|
-
}
|
|
7129
|
-
|
|
7130
|
-
.option-chain-widget .neutral {
|
|
7131
|
-
color: #6b7280;
|
|
7132
|
-
}
|
|
7133
|
-
|
|
7134
|
-
.option-chain-widget .widget-footer {
|
|
7135
|
-
background: #f9fafb;
|
|
7136
|
-
padding: 8px 16px;
|
|
7137
|
-
border-top: 1px solid #e5e7eb;
|
|
7138
|
-
text-align: center;
|
|
7139
|
-
font-size: 11px;
|
|
7140
|
-
color: #6b7280;
|
|
7408
|
+
.option-chain-widget .contract-cell.clickable {
|
|
7409
|
+
color: #3b82f6;
|
|
7410
|
+
cursor: pointer;
|
|
7411
|
+
text-decoration: underline;
|
|
7412
|
+
transition: all 0.2s ease;
|
|
7141
7413
|
}
|
|
7142
7414
|
|
|
7143
|
-
.option-chain-widget .
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
left: 0;
|
|
7147
|
-
right: 0;
|
|
7148
|
-
bottom: 0;
|
|
7149
|
-
background: rgba(255, 255, 255, 0.9);
|
|
7150
|
-
display: flex;
|
|
7151
|
-
align-items: center;
|
|
7152
|
-
justify-content: center;
|
|
7153
|
-
z-index: 20;
|
|
7415
|
+
.option-chain-widget .contract-cell.clickable:hover {
|
|
7416
|
+
color: #2563eb;
|
|
7417
|
+
background-color: #eff6ff;
|
|
7154
7418
|
}
|
|
7155
7419
|
|
|
7156
|
-
.option-chain-widget .
|
|
7157
|
-
|
|
7420
|
+
.option-chain-widget .contract-cell.clickable:focus {
|
|
7421
|
+
outline: 2px solid #3b82f6;
|
|
7422
|
+
outline-offset: 2px;
|
|
7423
|
+
border-radius: 2px;
|
|
7158
7424
|
}
|
|
7159
7425
|
|
|
7160
|
-
.option-chain-widget .
|
|
7161
|
-
|
|
7426
|
+
.option-chain-widget .contract-cell.clickable:active {
|
|
7427
|
+
color: #1e40af;
|
|
7428
|
+
background-color: #dbeafe;
|
|
7162
7429
|
}
|
|
7163
7430
|
|
|
7164
|
-
.option-chain-widget .
|
|
7165
|
-
|
|
7166
|
-
height: 32px;
|
|
7167
|
-
border: 3px solid #e5e7eb;
|
|
7168
|
-
border-top: 3px solid #3b82f6;
|
|
7169
|
-
border-radius: 50%;
|
|
7170
|
-
animation: spin 1s linear infinite;
|
|
7171
|
-
margin: 0 auto 12px;
|
|
7431
|
+
.option-chain-widget .positive {
|
|
7432
|
+
color: #059669;
|
|
7172
7433
|
}
|
|
7173
7434
|
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
100% { transform: rotate(360deg); }
|
|
7435
|
+
.option-chain-widget .negative {
|
|
7436
|
+
color: #dc2626;
|
|
7177
7437
|
}
|
|
7178
7438
|
|
|
7179
|
-
.option-chain-widget .
|
|
7180
|
-
padding: 40px;
|
|
7181
|
-
text-align: center;
|
|
7439
|
+
.option-chain-widget .neutral {
|
|
7182
7440
|
color: #6b7280;
|
|
7183
7441
|
}
|
|
7184
7442
|
|
|
7185
|
-
|
|
7186
|
-
padding: 20px;
|
|
7187
|
-
background: #fef2f2;
|
|
7188
|
-
color: #dc2626;
|
|
7189
|
-
border: 1px solid #fecaca;
|
|
7190
|
-
margin: 16px;
|
|
7191
|
-
border-radius: 4px;
|
|
7192
|
-
text-align: center;
|
|
7193
|
-
}
|
|
7443
|
+
/* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
|
|
7194
7444
|
|
|
7195
7445
|
/* RESPONSIVE STYLES */
|
|
7196
7446
|
|
|
@@ -7201,13 +7451,19 @@ ${SharedStyles}
|
|
|
7201
7451
|
align-items: stretch;
|
|
7202
7452
|
gap: 8px;
|
|
7203
7453
|
}
|
|
7204
|
-
|
|
7454
|
+
|
|
7455
|
+
.option-chain-widget .filter-section {
|
|
7456
|
+
flex-direction: row;
|
|
7457
|
+
margin-top: 0;
|
|
7458
|
+
}
|
|
7459
|
+
|
|
7205
7460
|
.option-chain-widget .symbol-input,
|
|
7206
|
-
.option-chain-widget .date-select
|
|
7461
|
+
.option-chain-widget .date-select,
|
|
7462
|
+
.option-chain-widget .strike-filter {
|
|
7207
7463
|
width: 100%;
|
|
7208
7464
|
max-width: none;
|
|
7209
7465
|
}
|
|
7210
|
-
|
|
7466
|
+
|
|
7211
7467
|
.option-chain-widget .fetch-button {
|
|
7212
7468
|
width: 100%;
|
|
7213
7469
|
}
|
|
@@ -7363,6 +7619,121 @@ ${SharedStyles}
|
|
|
7363
7619
|
padding: 2px 1px;
|
|
7364
7620
|
}
|
|
7365
7621
|
}
|
|
7622
|
+
|
|
7623
|
+
/* Modal Styles */
|
|
7624
|
+
.option-chain-modal-overlay {
|
|
7625
|
+
position: fixed;
|
|
7626
|
+
top: 0;
|
|
7627
|
+
left: 0;
|
|
7628
|
+
right: 0;
|
|
7629
|
+
bottom: 0;
|
|
7630
|
+
background: rgba(0, 0, 0, 0.6);
|
|
7631
|
+
display: flex;
|
|
7632
|
+
align-items: center;
|
|
7633
|
+
justify-content: center;
|
|
7634
|
+
z-index: 10000;
|
|
7635
|
+
padding: 20px;
|
|
7636
|
+
animation: fadeIn 0.2s ease-out;
|
|
7637
|
+
}
|
|
7638
|
+
|
|
7639
|
+
@keyframes fadeIn {
|
|
7640
|
+
from {
|
|
7641
|
+
opacity: 0;
|
|
7642
|
+
}
|
|
7643
|
+
to {
|
|
7644
|
+
opacity: 1;
|
|
7645
|
+
}
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
.option-chain-modal {
|
|
7649
|
+
background: white;
|
|
7650
|
+
border-radius: 12px;
|
|
7651
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
7652
|
+
max-width: 800px;
|
|
7653
|
+
width: 100%;
|
|
7654
|
+
max-height: 90vh;
|
|
7655
|
+
display: flex;
|
|
7656
|
+
flex-direction: column;
|
|
7657
|
+
animation: slideUp 0.3s ease-out;
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
@keyframes slideUp {
|
|
7661
|
+
from {
|
|
7662
|
+
transform: translateY(20px);
|
|
7663
|
+
opacity: 0;
|
|
7664
|
+
}
|
|
7665
|
+
to {
|
|
7666
|
+
transform: translateY(0);
|
|
7667
|
+
opacity: 1;
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
.option-chain-modal-header {
|
|
7672
|
+
display: flex;
|
|
7673
|
+
align-items: center;
|
|
7674
|
+
justify-content: space-between;
|
|
7675
|
+
padding: 20px 24px;
|
|
7676
|
+
border-bottom: 1px solid #e5e7eb;
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7679
|
+
.option-chain-modal-header h3 {
|
|
7680
|
+
margin: 0;
|
|
7681
|
+
font-size: 18px;
|
|
7682
|
+
font-weight: 600;
|
|
7683
|
+
color: #111827;
|
|
7684
|
+
}
|
|
7685
|
+
|
|
7686
|
+
.option-chain-modal-close {
|
|
7687
|
+
background: none;
|
|
7688
|
+
border: none;
|
|
7689
|
+
font-size: 28px;
|
|
7690
|
+
line-height: 1;
|
|
7691
|
+
color: #6b7280;
|
|
7692
|
+
cursor: pointer;
|
|
7693
|
+
padding: 0;
|
|
7694
|
+
width: 32px;
|
|
7695
|
+
height: 32px;
|
|
7696
|
+
display: flex;
|
|
7697
|
+
align-items: center;
|
|
7698
|
+
justify-content: center;
|
|
7699
|
+
border-radius: 4px;
|
|
7700
|
+
transition: all 0.2s ease;
|
|
7701
|
+
}
|
|
7702
|
+
|
|
7703
|
+
.option-chain-modal-close:hover {
|
|
7704
|
+
background: #f3f4f6;
|
|
7705
|
+
color: #111827;
|
|
7706
|
+
}
|
|
7707
|
+
|
|
7708
|
+
.option-chain-modal-close:active {
|
|
7709
|
+
background: #e5e7eb;
|
|
7710
|
+
}
|
|
7711
|
+
|
|
7712
|
+
.option-chain-modal-body {
|
|
7713
|
+
flex: 1;
|
|
7714
|
+
overflow-y: auto;
|
|
7715
|
+
padding: 24px;
|
|
7716
|
+
}
|
|
7717
|
+
|
|
7718
|
+
/* Responsive modal */
|
|
7719
|
+
@media (max-width: 768px) {
|
|
7720
|
+
.option-chain-modal {
|
|
7721
|
+
max-width: 95%;
|
|
7722
|
+
max-height: 95vh;
|
|
7723
|
+
}
|
|
7724
|
+
|
|
7725
|
+
.option-chain-modal-header {
|
|
7726
|
+
padding: 16px;
|
|
7727
|
+
}
|
|
7728
|
+
|
|
7729
|
+
.option-chain-modal-header h3 {
|
|
7730
|
+
font-size: 16px;
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7733
|
+
.option-chain-modal-body {
|
|
7734
|
+
padding: 16px;
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7366
7737
|
`;
|
|
7367
7738
|
|
|
7368
7739
|
class OptionChainWidget extends BaseWidget {
|
|
@@ -7377,7 +7748,18 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7377
7748
|
this.data = null;
|
|
7378
7749
|
this.isDestroyed = false;
|
|
7379
7750
|
this.unsubscribe = null;
|
|
7751
|
+
this.unsubscribeUnderlying = null; // For underlying stock price
|
|
7752
|
+
this.underlyingPrice = null; // Current price of underlying stock
|
|
7380
7753
|
this.loadingTimeout = null;
|
|
7754
|
+
this.renderDebounceTimeout = null; // For debouncing re-renders
|
|
7755
|
+
|
|
7756
|
+
// Separate cache for option chain data and underlying price
|
|
7757
|
+
this.cachedOptionChainData = null;
|
|
7758
|
+
this.cachedUnderlyingPrice = null;
|
|
7759
|
+
|
|
7760
|
+
// Modal for Options widget
|
|
7761
|
+
this.optionsModal = null;
|
|
7762
|
+
this.optionsWidgetInstance = null;
|
|
7381
7763
|
|
|
7382
7764
|
// INPUT VALIDATION: Validate initial symbol if provided
|
|
7383
7765
|
if (options.symbol) {
|
|
@@ -7393,6 +7775,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7393
7775
|
}
|
|
7394
7776
|
this.date = '';
|
|
7395
7777
|
this.availableDates = {};
|
|
7778
|
+
this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
|
|
7396
7779
|
|
|
7397
7780
|
// RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
|
|
7398
7781
|
this.fetchRateLimiter = createRateLimitValidator(1000);
|
|
@@ -7423,6 +7806,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7423
7806
|
this.dateSelect = this.container.querySelector('.date-select');
|
|
7424
7807
|
this.fetchButton = this.container.querySelector('.fetch-button');
|
|
7425
7808
|
this.dataGrid = this.container.querySelector('.option-chain-data-grid');
|
|
7809
|
+
this.strikeFilterSelect = this.container.querySelector('.strike-filter');
|
|
7426
7810
|
|
|
7427
7811
|
// Set initial symbol value if provided in options
|
|
7428
7812
|
if (this.symbol) {
|
|
@@ -7497,6 +7881,22 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7497
7881
|
}
|
|
7498
7882
|
this.loadAvailableDates(symbolValidation.sanitized);
|
|
7499
7883
|
});
|
|
7884
|
+
|
|
7885
|
+
// MEMORY LEAK FIX: Use BaseWidget's addEventListener
|
|
7886
|
+
// Strike filter dropdown
|
|
7887
|
+
this.addEventListener(this.strikeFilterSelect, 'change', e => {
|
|
7888
|
+
const value = e.target.value;
|
|
7889
|
+
if (value === 'all') {
|
|
7890
|
+
this.strikeFilterRange = 'all';
|
|
7891
|
+
} else {
|
|
7892
|
+
this.strikeFilterRange = parseInt(value, 10);
|
|
7893
|
+
}
|
|
7894
|
+
|
|
7895
|
+
// Re-render with new filter if we have data
|
|
7896
|
+
if (this.data) {
|
|
7897
|
+
this.displayOptionChain(this.data);
|
|
7898
|
+
}
|
|
7899
|
+
});
|
|
7500
7900
|
}
|
|
7501
7901
|
async loadAvailableDates(symbol) {
|
|
7502
7902
|
if (!symbol) return;
|
|
@@ -7512,7 +7912,9 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7512
7912
|
// Use API service from wsManager
|
|
7513
7913
|
const apiService = this.wsManager.getApiService();
|
|
7514
7914
|
const data = await apiService.getOptionChainDates(symbol);
|
|
7515
|
-
|
|
7915
|
+
if (this.debug) {
|
|
7916
|
+
console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
|
|
7917
|
+
}
|
|
7516
7918
|
this.availableDates = data.dates_dictionary || {};
|
|
7517
7919
|
this.populateDateOptions();
|
|
7518
7920
|
|
|
@@ -7606,11 +8008,15 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7606
8008
|
this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
|
|
7607
8009
|
}, 10000); // 10 second timeout
|
|
7608
8010
|
|
|
7609
|
-
// Unsubscribe from previous
|
|
8011
|
+
// Unsubscribe from previous subscriptions if they exist
|
|
7610
8012
|
if (this.unsubscribe) {
|
|
7611
8013
|
this.unsubscribe();
|
|
7612
8014
|
this.unsubscribe = null;
|
|
7613
8015
|
}
|
|
8016
|
+
if (this.unsubscribeUnderlying) {
|
|
8017
|
+
this.unsubscribeUnderlying();
|
|
8018
|
+
this.unsubscribeUnderlying = null;
|
|
8019
|
+
}
|
|
7614
8020
|
|
|
7615
8021
|
// Subscribe to option chain data
|
|
7616
8022
|
this.subscribeToData();
|
|
@@ -7620,25 +8026,62 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7620
8026
|
}
|
|
7621
8027
|
}
|
|
7622
8028
|
subscribeToData() {
|
|
7623
|
-
// Subscribe
|
|
7624
|
-
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
|
|
7625
|
-
// Pass symbol as string
|
|
7626
|
-
{
|
|
8029
|
+
// Subscribe to option chain data
|
|
8030
|
+
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
|
|
7627
8031
|
date: this.date
|
|
7628
|
-
}
|
|
7629
|
-
);
|
|
8032
|
+
});
|
|
7630
8033
|
|
|
7631
|
-
//
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
8034
|
+
// Subscribe to underlying stock price for ITM highlighting
|
|
8035
|
+
this.unsubscribeUnderlying = this.wsManager.subscribe(`${this.widgetId}-underlying`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
|
|
8036
|
+
}
|
|
8037
|
+
|
|
8038
|
+
/**
|
|
8039
|
+
* Find the nearest strike price to the underlying price (ATM strike)
|
|
8040
|
+
* @param {Array} sortedStrikes - Array of strike prices sorted ascending
|
|
8041
|
+
* @param {Number} underlyingPrice - Current price of underlying stock
|
|
8042
|
+
* @returns {String} - The nearest strike price
|
|
8043
|
+
*/
|
|
8044
|
+
findNearestStrike(sortedStrikes, underlyingPrice) {
|
|
8045
|
+
if (!sortedStrikes || sortedStrikes.length === 0 || !underlyingPrice) {
|
|
8046
|
+
return null;
|
|
8047
|
+
}
|
|
8048
|
+
|
|
8049
|
+
// Find strike closest to underlying price
|
|
8050
|
+
let nearestStrike = sortedStrikes[0];
|
|
8051
|
+
let minDifference = Math.abs(parseFloat(sortedStrikes[0]) - underlyingPrice);
|
|
8052
|
+
for (const strike of sortedStrikes) {
|
|
8053
|
+
const difference = Math.abs(parseFloat(strike) - underlyingPrice);
|
|
8054
|
+
if (difference < minDifference) {
|
|
8055
|
+
minDifference = difference;
|
|
8056
|
+
nearestStrike = strike;
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
return nearestStrike;
|
|
8060
|
+
}
|
|
8061
|
+
|
|
8062
|
+
/**
|
|
8063
|
+
* Filter strikes to show only those near the money
|
|
8064
|
+
* @param {Array} sortedStrikes - Array of strike prices sorted ascending
|
|
8065
|
+
* @param {String} atmStrike - The at-the-money strike price
|
|
8066
|
+
* @param {Number} range - Number of strikes to show above and below ATM
|
|
8067
|
+
* @returns {Array} - Filtered array of strikes
|
|
8068
|
+
*/
|
|
8069
|
+
filterNearMoneyStrikes(sortedStrikes, atmStrike, range) {
|
|
8070
|
+
if (!atmStrike || !sortedStrikes || sortedStrikes.length === 0) {
|
|
8071
|
+
return sortedStrikes;
|
|
8072
|
+
}
|
|
8073
|
+
const atmIndex = sortedStrikes.indexOf(atmStrike);
|
|
8074
|
+
if (atmIndex === -1) {
|
|
8075
|
+
return sortedStrikes;
|
|
8076
|
+
}
|
|
8077
|
+
const startIndex = Math.max(0, atmIndex - range);
|
|
8078
|
+
const endIndex = Math.min(sortedStrikes.length - 1, atmIndex + range);
|
|
8079
|
+
return sortedStrikes.slice(startIndex, endIndex + 1);
|
|
7637
8080
|
}
|
|
7638
8081
|
handleData(message) {
|
|
7639
|
-
// Clear loading timeout since we received data
|
|
8082
|
+
// Clear loading timeout since we received data (use BaseWidget method)
|
|
7640
8083
|
if (this.loadingTimeout) {
|
|
7641
|
-
clearTimeout(this.loadingTimeout);
|
|
8084
|
+
this.clearTimeout(this.loadingTimeout);
|
|
7642
8085
|
this.loadingTimeout = null;
|
|
7643
8086
|
}
|
|
7644
8087
|
|
|
@@ -7670,7 +8113,9 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7670
8113
|
}
|
|
7671
8114
|
}
|
|
7672
8115
|
} else {
|
|
7673
|
-
|
|
8116
|
+
// Cache the option chain data
|
|
8117
|
+
this.data = message;
|
|
8118
|
+
this.cachedOptionChainData = message;
|
|
7674
8119
|
this.displayOptionChain(message);
|
|
7675
8120
|
}
|
|
7676
8121
|
} else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
|
|
@@ -7685,9 +8130,35 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7685
8130
|
}
|
|
7686
8131
|
}
|
|
7687
8132
|
} else {
|
|
7688
|
-
|
|
8133
|
+
// Cache the option chain data
|
|
8134
|
+
this.data = message.data;
|
|
8135
|
+
this.cachedOptionChainData = message.data;
|
|
7689
8136
|
this.displayOptionChain(message.data);
|
|
7690
8137
|
}
|
|
8138
|
+
} else if (message.type === 'queryl1' && message.Data) {
|
|
8139
|
+
if (this.debug) {
|
|
8140
|
+
console.log('[OptionChainWidget] Received underlying price update');
|
|
8141
|
+
}
|
|
8142
|
+
const data = message.Data.find(d => d.Symbol === this.symbol);
|
|
8143
|
+
if (data && data.LastPx) {
|
|
8144
|
+
// Cache the underlying price
|
|
8145
|
+
this.underlyingPrice = parseFloat(data.LastPx);
|
|
8146
|
+
this.cachedUnderlyingPrice = parseFloat(data.LastPx);
|
|
8147
|
+
|
|
8148
|
+
// Debounce re-render to prevent excessive updates
|
|
8149
|
+
// Clear any pending render
|
|
8150
|
+
if (this.renderDebounceTimeout) {
|
|
8151
|
+
this.clearTimeout(this.renderDebounceTimeout);
|
|
8152
|
+
}
|
|
8153
|
+
|
|
8154
|
+
// Schedule a debounced re-render (500ms delay)
|
|
8155
|
+
this.renderDebounceTimeout = this.setTimeout(() => {
|
|
8156
|
+
if (this.data) {
|
|
8157
|
+
this.displayOptionChain(this.data);
|
|
8158
|
+
}
|
|
8159
|
+
this.renderDebounceTimeout = null;
|
|
8160
|
+
}, 500);
|
|
8161
|
+
}
|
|
7691
8162
|
}
|
|
7692
8163
|
if (message._cached) {
|
|
7693
8164
|
this.showConnectionQuality(); // Show cache indicator
|
|
@@ -7695,6 +8166,14 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7695
8166
|
}
|
|
7696
8167
|
displayOptionChain(data) {
|
|
7697
8168
|
if (this.isDestroyed) return;
|
|
8169
|
+
|
|
8170
|
+
// Validate data
|
|
8171
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
8172
|
+
if (this.debug) {
|
|
8173
|
+
console.warn('[OptionChainWidget] Invalid or empty data passed to displayOptionChain');
|
|
8174
|
+
}
|
|
8175
|
+
return;
|
|
8176
|
+
}
|
|
7698
8177
|
try {
|
|
7699
8178
|
// Hide loading overlay
|
|
7700
8179
|
this.hideLoading();
|
|
@@ -7727,9 +8206,18 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7727
8206
|
}
|
|
7728
8207
|
});
|
|
7729
8208
|
|
|
7730
|
-
// Sort strikes
|
|
8209
|
+
// Sort strikes
|
|
7731
8210
|
const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
7732
|
-
|
|
8211
|
+
|
|
8212
|
+
// Apply ATM filter if not showing all
|
|
8213
|
+
let displayStrikes = sortedStrikes;
|
|
8214
|
+
if (this.strikeFilterRange !== 'all' && this.underlyingPrice) {
|
|
8215
|
+
const atmStrike = this.findNearestStrike(sortedStrikes, this.underlyingPrice);
|
|
8216
|
+
if (atmStrike) {
|
|
8217
|
+
displayStrikes = this.filterNearMoneyStrikes(sortedStrikes, atmStrike, this.strikeFilterRange);
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
displayStrikes.forEach(strike => {
|
|
7733
8221
|
const {
|
|
7734
8222
|
call,
|
|
7735
8223
|
put
|
|
@@ -7748,7 +8236,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7748
8236
|
const formatted = parseFloat(change).toFixed(2);
|
|
7749
8237
|
const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
|
|
7750
8238
|
const sign = change > 0 ? '+' : '';
|
|
7751
|
-
span.className =
|
|
8239
|
+
span.className = className;
|
|
7752
8240
|
span.textContent = `${sign}${formatted}`;
|
|
7753
8241
|
return span;
|
|
7754
8242
|
};
|
|
@@ -7761,7 +8249,28 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7761
8249
|
// Create calls data section
|
|
7762
8250
|
const callsData = document.createElement('div');
|
|
7763
8251
|
callsData.className = 'calls-data';
|
|
7764
|
-
|
|
8252
|
+
|
|
8253
|
+
// Add ITM highlighting for calls (ITM when strike < underlying price)
|
|
8254
|
+
if (this.underlyingPrice && parseFloat(strike) < this.underlyingPrice) {
|
|
8255
|
+
callsData.classList.add('in-the-money');
|
|
8256
|
+
}
|
|
8257
|
+
|
|
8258
|
+
// Make contract cell clickable
|
|
8259
|
+
const callContractCell = createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell');
|
|
8260
|
+
if (call && call.symbol) {
|
|
8261
|
+
callContractCell.classList.add('clickable');
|
|
8262
|
+
callContractCell.setAttribute('role', 'button');
|
|
8263
|
+
callContractCell.setAttribute('tabindex', '0');
|
|
8264
|
+
callContractCell.setAttribute('data-symbol', call.symbol);
|
|
8265
|
+
this.addEventListener(callContractCell, 'click', () => this.showOptionsModal(call.symbol));
|
|
8266
|
+
this.addEventListener(callContractCell, 'keypress', e => {
|
|
8267
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
8268
|
+
e.preventDefault();
|
|
8269
|
+
this.showOptionsModal(call.symbol);
|
|
8270
|
+
}
|
|
8271
|
+
});
|
|
8272
|
+
}
|
|
8273
|
+
callsData.appendChild(callContractCell);
|
|
7765
8274
|
callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
|
|
7766
8275
|
const callChangeSpan = document.createElement('span');
|
|
7767
8276
|
if (call) {
|
|
@@ -7784,7 +8293,28 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7784
8293
|
// Create puts data section
|
|
7785
8294
|
const putsData = document.createElement('div');
|
|
7786
8295
|
putsData.className = 'puts-data';
|
|
7787
|
-
|
|
8296
|
+
|
|
8297
|
+
// Add ITM highlighting for puts (ITM when strike > underlying price)
|
|
8298
|
+
if (this.underlyingPrice && parseFloat(strike) > this.underlyingPrice) {
|
|
8299
|
+
putsData.classList.add('in-the-money');
|
|
8300
|
+
}
|
|
8301
|
+
|
|
8302
|
+
// Make contract cell clickable
|
|
8303
|
+
const putContractCell = createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell');
|
|
8304
|
+
if (put && put.symbol) {
|
|
8305
|
+
putContractCell.classList.add('clickable');
|
|
8306
|
+
putContractCell.setAttribute('role', 'button');
|
|
8307
|
+
putContractCell.setAttribute('tabindex', '0');
|
|
8308
|
+
putContractCell.setAttribute('data-symbol', put.symbol);
|
|
8309
|
+
this.addEventListener(putContractCell, 'click', () => this.showOptionsModal(put.symbol));
|
|
8310
|
+
this.addEventListener(putContractCell, 'keypress', e => {
|
|
8311
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
8312
|
+
e.preventDefault();
|
|
8313
|
+
this.showOptionsModal(put.symbol);
|
|
8314
|
+
}
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8317
|
+
putsData.appendChild(putContractCell);
|
|
7788
8318
|
putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
|
|
7789
8319
|
const putChangeSpan = document.createElement('span');
|
|
7790
8320
|
if (put) {
|
|
@@ -7808,14 +8338,22 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7808
8338
|
|
|
7809
8339
|
// Add footer - with array length check
|
|
7810
8340
|
if (data && data.length > 0) {
|
|
7811
|
-
const
|
|
8341
|
+
const isDelayed = data[0].DataSource === 'OPRA-D';
|
|
8342
|
+
|
|
8343
|
+
// Get timestamp - subtract 20 minutes if delayed data
|
|
8344
|
+
let timestampValue = data[0].quoteTime || Date.now();
|
|
8345
|
+
if (isDelayed) {
|
|
8346
|
+
// Subtract 20 minutes (20 * 60 * 1000 milliseconds)
|
|
8347
|
+
timestampValue = timestampValue - 20 * 60 * 1000;
|
|
8348
|
+
}
|
|
8349
|
+
const timestamp = formatTimestampET(timestampValue);
|
|
7812
8350
|
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
7813
8351
|
if (lastUpdateElement) {
|
|
7814
8352
|
lastUpdateElement.textContent = `Last update: ${timestamp}`;
|
|
7815
8353
|
}
|
|
7816
8354
|
|
|
7817
8355
|
// Update footer
|
|
7818
|
-
const source =
|
|
8356
|
+
const source = isDelayed ? '20 mins delayed' : 'Real-time';
|
|
7819
8357
|
const dataSourceElement = this.container.querySelector('.data-source');
|
|
7820
8358
|
if (dataSourceElement) {
|
|
7821
8359
|
dataSourceElement.textContent = `Source: ${source}`;
|
|
@@ -7838,35 +8376,6 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7838
8376
|
loadingOverlay.classList.add('hidden');
|
|
7839
8377
|
}
|
|
7840
8378
|
}
|
|
7841
|
-
|
|
7842
|
-
/* showInputError(message) {
|
|
7843
|
-
const errorDiv = document.createElement('div');
|
|
7844
|
-
errorDiv.className = 'symbol-error-message';
|
|
7845
|
-
errorDiv.textContent = message;
|
|
7846
|
-
errorDiv.style.cssText = `
|
|
7847
|
-
color: #dc2626;
|
|
7848
|
-
font-size: 12px;
|
|
7849
|
-
margin-bottom: 2px;
|
|
7850
|
-
position: absolute;
|
|
7851
|
-
background: white;
|
|
7852
|
-
z-index: 1000;
|
|
7853
|
-
transform: translateY(-100%);
|
|
7854
|
-
top: -4px;
|
|
7855
|
-
left: 0; // Align with input box
|
|
7856
|
-
`;
|
|
7857
|
-
|
|
7858
|
-
// Ensure the parent is positioned
|
|
7859
|
-
const parent = this.symbolInput;
|
|
7860
|
-
if (parent) {
|
|
7861
|
-
parent.style.position = 'relative';
|
|
7862
|
-
// Insert the errorDiv as the first child of the parent
|
|
7863
|
-
parent.insertBefore(errorDiv, parent.firstChild);
|
|
7864
|
-
}
|
|
7865
|
-
|
|
7866
|
-
this.symbolInput.errorMessage = errorDiv;
|
|
7867
|
-
this.symbolInput.focus();
|
|
7868
|
-
} */
|
|
7869
|
-
|
|
7870
8379
|
showError(message) {
|
|
7871
8380
|
this.hideLoading();
|
|
7872
8381
|
this.clearError();
|
|
@@ -7939,19 +8448,135 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7939
8448
|
this.container.appendChild(noDataElement);
|
|
7940
8449
|
}
|
|
7941
8450
|
}
|
|
8451
|
+
|
|
8452
|
+
/**
|
|
8453
|
+
* Show Options widget in a modal for a specific option contract
|
|
8454
|
+
* @param {string} optionSymbol - The option contract symbol
|
|
8455
|
+
*/
|
|
8456
|
+
showOptionsModal(optionSymbol) {
|
|
8457
|
+
if (!optionSymbol || optionSymbol === '--') {
|
|
8458
|
+
return;
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
// Close existing modal if any
|
|
8462
|
+
this.closeOptionsModal();
|
|
8463
|
+
|
|
8464
|
+
// Create modal overlay
|
|
8465
|
+
this.optionsModal = document.createElement('div');
|
|
8466
|
+
this.optionsModal.className = 'option-chain-modal-overlay';
|
|
8467
|
+
this.optionsModal.innerHTML = `
|
|
8468
|
+
<div class="option-chain-modal">
|
|
8469
|
+
<div class="option-chain-modal-header">
|
|
8470
|
+
<h3>Option Details: ${sanitizeSymbol(optionSymbol)}</h3>
|
|
8471
|
+
<button class="option-chain-modal-close" aria-label="Close modal">×</button>
|
|
8472
|
+
</div>
|
|
8473
|
+
<div class="option-chain-modal-body">
|
|
8474
|
+
<div id="option-chain-modal-widget"></div>
|
|
8475
|
+
</div>
|
|
8476
|
+
</div>
|
|
8477
|
+
`;
|
|
8478
|
+
|
|
8479
|
+
// Add to body
|
|
8480
|
+
document.body.appendChild(this.optionsModal);
|
|
8481
|
+
|
|
8482
|
+
// Add close event
|
|
8483
|
+
const closeBtn = this.optionsModal.querySelector('.option-chain-modal-close');
|
|
8484
|
+
this.addEventListener(closeBtn, 'click', () => this.closeOptionsModal());
|
|
8485
|
+
|
|
8486
|
+
// Close on overlay click
|
|
8487
|
+
this.addEventListener(this.optionsModal, 'click', e => {
|
|
8488
|
+
if (e.target === this.optionsModal) {
|
|
8489
|
+
this.closeOptionsModal();
|
|
8490
|
+
}
|
|
8491
|
+
});
|
|
8492
|
+
|
|
8493
|
+
// Close on Escape key
|
|
8494
|
+
const escapeHandler = e => {
|
|
8495
|
+
if (e.key === 'Escape') {
|
|
8496
|
+
this.closeOptionsModal();
|
|
8497
|
+
}
|
|
8498
|
+
};
|
|
8499
|
+
document.addEventListener('keydown', escapeHandler);
|
|
8500
|
+
this._escapeHandler = escapeHandler;
|
|
8501
|
+
|
|
8502
|
+
// Create Options widget instance
|
|
8503
|
+
try {
|
|
8504
|
+
const widgetContainer = this.optionsModal.querySelector('#option-chain-modal-widget');
|
|
8505
|
+
this.optionsWidgetInstance = new OptionsWidget(widgetContainer, {
|
|
8506
|
+
symbol: optionSymbol,
|
|
8507
|
+
wsManager: this.wsManager,
|
|
8508
|
+
debug: this.debug,
|
|
8509
|
+
styled: true
|
|
8510
|
+
}, `${this.widgetId}-options-modal`);
|
|
8511
|
+
if (this.debug) {
|
|
8512
|
+
console.log('[OptionChainWidget] Options modal opened for:', optionSymbol);
|
|
8513
|
+
}
|
|
8514
|
+
} catch (error) {
|
|
8515
|
+
console.error('[OptionChainWidget] Error creating Options widget:', error);
|
|
8516
|
+
this.closeOptionsModal();
|
|
8517
|
+
this.showError(`Failed to load option details: ${error.message}`);
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
|
|
8521
|
+
/**
|
|
8522
|
+
* Close the Options modal and cleanup
|
|
8523
|
+
*/
|
|
8524
|
+
closeOptionsModal() {
|
|
8525
|
+
// Destroy widget instance
|
|
8526
|
+
if (this.optionsWidgetInstance) {
|
|
8527
|
+
this.optionsWidgetInstance.destroy();
|
|
8528
|
+
this.optionsWidgetInstance = null;
|
|
8529
|
+
}
|
|
8530
|
+
|
|
8531
|
+
// Remove modal from DOM
|
|
8532
|
+
if (this.optionsModal) {
|
|
8533
|
+
this.optionsModal.remove();
|
|
8534
|
+
this.optionsModal = null;
|
|
8535
|
+
}
|
|
8536
|
+
|
|
8537
|
+
// Remove escape key handler
|
|
8538
|
+
if (this._escapeHandler) {
|
|
8539
|
+
document.removeEventListener('keydown', this._escapeHandler);
|
|
8540
|
+
this._escapeHandler = null;
|
|
8541
|
+
}
|
|
8542
|
+
if (this.debug) {
|
|
8543
|
+
console.log('[OptionChainWidget] Options modal closed');
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
7942
8546
|
destroy() {
|
|
7943
8547
|
this.isDestroyed = true;
|
|
8548
|
+
|
|
8549
|
+
// Close modal if open
|
|
8550
|
+
this.closeOptionsModal();
|
|
7944
8551
|
if (this.unsubscribe) {
|
|
7945
8552
|
this.unsubscribe();
|
|
7946
8553
|
this.unsubscribe = null;
|
|
7947
8554
|
}
|
|
7948
8555
|
|
|
8556
|
+
// Unsubscribe from underlying price
|
|
8557
|
+
if (this.unsubscribeUnderlying) {
|
|
8558
|
+
this.unsubscribeUnderlying();
|
|
8559
|
+
this.unsubscribeUnderlying = null;
|
|
8560
|
+
}
|
|
8561
|
+
|
|
7949
8562
|
// MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
|
|
7950
8563
|
if (this.loadingTimeout) {
|
|
7951
8564
|
this.clearTimeout(this.loadingTimeout);
|
|
7952
8565
|
this.loadingTimeout = null;
|
|
7953
8566
|
}
|
|
7954
8567
|
|
|
8568
|
+
// Clear debounce timeout
|
|
8569
|
+
if (this.renderDebounceTimeout) {
|
|
8570
|
+
this.clearTimeout(this.renderDebounceTimeout);
|
|
8571
|
+
this.renderDebounceTimeout = null;
|
|
8572
|
+
}
|
|
8573
|
+
|
|
8574
|
+
// Clear cached data
|
|
8575
|
+
this.cachedOptionChainData = null;
|
|
8576
|
+
this.cachedUnderlyingPrice = null;
|
|
8577
|
+
this.underlyingPrice = null;
|
|
8578
|
+
this.data = null;
|
|
8579
|
+
|
|
7955
8580
|
// Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
|
|
7956
8581
|
super.destroy();
|
|
7957
8582
|
}
|
|
@@ -8037,80 +8662,81 @@ const DataStyles = `
|
|
|
8037
8662
|
font-family: Arial, sans-serif;
|
|
8038
8663
|
}
|
|
8039
8664
|
|
|
8040
|
-
.widget-header {
|
|
8665
|
+
.data-widget .widget-header {
|
|
8041
8666
|
display: flex;
|
|
8042
8667
|
flex-direction: column;
|
|
8043
8668
|
margin-bottom: 10px;
|
|
8044
8669
|
}
|
|
8045
8670
|
|
|
8046
|
-
.symbol {
|
|
8671
|
+
.data-widget .symbol {
|
|
8047
8672
|
font-size: 24px;
|
|
8048
8673
|
font-weight: bold;
|
|
8049
8674
|
margin-right: 10px;
|
|
8050
8675
|
}
|
|
8051
8676
|
|
|
8052
|
-
.company-info {
|
|
8677
|
+
.data-widget .company-info {
|
|
8053
8678
|
font-size: 12px;
|
|
8054
8679
|
color: #666;
|
|
8055
8680
|
}
|
|
8056
8681
|
|
|
8057
|
-
.trading-info {
|
|
8682
|
+
.data-widget .trading-info {
|
|
8058
8683
|
font-size: 12px;
|
|
8059
8684
|
color: #999;
|
|
8060
8685
|
}
|
|
8061
8686
|
|
|
8062
|
-
.price-section {
|
|
8687
|
+
.data-widget .price-section {
|
|
8063
8688
|
display: flex;
|
|
8064
8689
|
align-items: baseline;
|
|
8065
8690
|
margin-bottom: 10px;
|
|
8066
8691
|
}
|
|
8067
8692
|
|
|
8068
|
-
.current-price {
|
|
8693
|
+
.data-widget .current-price {
|
|
8069
8694
|
font-size: 36px;
|
|
8070
8695
|
font-weight: bold;
|
|
8071
8696
|
margin-right: 10px;
|
|
8072
8697
|
}
|
|
8073
8698
|
|
|
8074
|
-
.price-change {
|
|
8699
|
+
.data-widget .price-change {
|
|
8075
8700
|
font-size: 18px;
|
|
8076
8701
|
}
|
|
8077
8702
|
|
|
8078
|
-
.price-change.positive {
|
|
8703
|
+
.data-widget .price-change.positive {
|
|
8079
8704
|
color: green;
|
|
8080
8705
|
}
|
|
8081
8706
|
|
|
8082
|
-
.price-change.negative {
|
|
8707
|
+
.data-widget .price-change.negative {
|
|
8083
8708
|
color: red;
|
|
8084
8709
|
}
|
|
8085
8710
|
|
|
8086
|
-
.bid-ask-section {
|
|
8711
|
+
.data-widget .bid-ask-section {
|
|
8087
8712
|
display: flex;
|
|
8088
8713
|
justify-content: space-between;
|
|
8089
8714
|
margin-bottom: 10px;
|
|
8090
8715
|
}
|
|
8091
8716
|
|
|
8092
|
-
.ask,
|
|
8717
|
+
.data-widget .ask,
|
|
8718
|
+
.data-widget .bid {
|
|
8093
8719
|
display: flex;
|
|
8094
8720
|
align-items: center;
|
|
8095
8721
|
}
|
|
8096
8722
|
|
|
8097
|
-
.label {
|
|
8723
|
+
.data-widget .label {
|
|
8098
8724
|
font-size: 14px;
|
|
8099
8725
|
margin-right: 5px;
|
|
8100
8726
|
}
|
|
8101
8727
|
|
|
8102
|
-
.value {
|
|
8728
|
+
.data-widget .value {
|
|
8103
8729
|
font-size: 16px;
|
|
8104
8730
|
font-weight: bold;
|
|
8105
8731
|
margin-right: 5px;
|
|
8106
8732
|
}
|
|
8107
8733
|
|
|
8108
|
-
.size {
|
|
8734
|
+
.data-widget .size {
|
|
8109
8735
|
font-size: 14px;
|
|
8110
8736
|
color: red;
|
|
8111
8737
|
}
|
|
8112
8738
|
|
|
8113
|
-
.widget-footer {
|
|
8739
|
+
.data-widget .widget-footer {
|
|
8114
8740
|
font-size: 12px;
|
|
8115
8741
|
color: #333;
|
|
8116
8742
|
}
|
|
@@ -8521,76 +9147,50 @@ class CombinedMarketWidget extends BaseWidget {
|
|
|
8521
9147
|
|
|
8522
9148
|
// Subscribe to regular market data (Level 1)
|
|
8523
9149
|
// Create a wrapper to add data type context
|
|
8524
|
-
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'],
|
|
8525
|
-
const {
|
|
8526
|
-
event,
|
|
8527
|
-
data
|
|
8528
|
-
} = messageWrapper;
|
|
8529
|
-
|
|
8530
|
-
// Handle connection events
|
|
8531
|
-
if (event === 'connection') {
|
|
8532
|
-
this.handleConnectionStatus(data);
|
|
8533
|
-
return;
|
|
8534
|
-
}
|
|
8535
|
-
|
|
8536
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8537
|
-
if (event === 'data') {
|
|
8538
|
-
data._dataType = 'market';
|
|
8539
|
-
this.handleMessage({
|
|
8540
|
-
event,
|
|
8541
|
-
data
|
|
8542
|
-
});
|
|
8543
|
-
}
|
|
8544
|
-
}, this.symbol);
|
|
9150
|
+
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
|
|
8545
9151
|
|
|
8546
9152
|
// Subscribe to night session data (BlueOcean or Bruce)
|
|
8547
|
-
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'],
|
|
8548
|
-
const {
|
|
8549
|
-
event,
|
|
8550
|
-
data
|
|
8551
|
-
} = messageWrapper;
|
|
8552
|
-
|
|
8553
|
-
// Connection already handled by market subscription
|
|
8554
|
-
if (event === 'connection') {
|
|
8555
|
-
return;
|
|
8556
|
-
}
|
|
8557
|
-
|
|
8558
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8559
|
-
if (event === 'data') {
|
|
8560
|
-
data._dataType = 'night';
|
|
8561
|
-
this.handleMessage({
|
|
8562
|
-
event,
|
|
8563
|
-
data
|
|
8564
|
-
});
|
|
8565
|
-
}
|
|
8566
|
-
}, this.symbol);
|
|
9153
|
+
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
|
|
8567
9154
|
}
|
|
8568
9155
|
handleData(message) {
|
|
8569
9156
|
// Extract data type from metadata
|
|
8570
|
-
const dataType = message.
|
|
9157
|
+
const dataType = message.type;
|
|
8571
9158
|
if (this.debug) {
|
|
8572
9159
|
console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
|
|
8573
9160
|
}
|
|
8574
9161
|
|
|
8575
|
-
//
|
|
8576
|
-
if (
|
|
8577
|
-
|
|
8578
|
-
|
|
9162
|
+
// Clear loading timeout since we received data (use BaseWidget method)
|
|
9163
|
+
if (this.loadingTimeout) {
|
|
9164
|
+
this.clearTimeout(this.loadingTimeout);
|
|
9165
|
+
this.loadingTimeout = null;
|
|
9166
|
+
}
|
|
9167
|
+
|
|
9168
|
+
// Handle option chain data
|
|
9169
|
+
if (Array.isArray(message)) {
|
|
9170
|
+
if (message.length === 0) {
|
|
9171
|
+
// Only show no data state if we don't have cached data
|
|
9172
|
+
if (!this.data) {
|
|
9173
|
+
this.showNoDataState();
|
|
9174
|
+
} else {
|
|
9175
|
+
this.hideLoading();
|
|
9176
|
+
if (this.debug) {
|
|
9177
|
+
console.log('[OptionChainWidget] No new data, keeping cached data visible');
|
|
9178
|
+
}
|
|
9179
|
+
}
|
|
8579
9180
|
}
|
|
8580
|
-
return;
|
|
8581
9181
|
}
|
|
8582
9182
|
|
|
8583
9183
|
// Handle error messages from server
|
|
8584
|
-
if (message.type === 'error'
|
|
9184
|
+
if (message.type === 'error' || message.error == true) {
|
|
8585
9185
|
if (this.debug) {
|
|
8586
9186
|
console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
|
|
8587
9187
|
}
|
|
8588
9188
|
// Keep existing cached data visible, just hide loading
|
|
8589
|
-
if (dataType === '
|
|
9189
|
+
if (dataType === 'queryl1' && !this.marketData) {
|
|
8590
9190
|
if (this.debug) {
|
|
8591
9191
|
console.log('[CombinedMarketWidget] No market data available');
|
|
8592
9192
|
}
|
|
8593
|
-
} else if (dataType === '
|
|
9193
|
+
} else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
|
|
8594
9194
|
if (this.debug) {
|
|
8595
9195
|
console.log('[CombinedMarketWidget] No night session data available');
|
|
8596
9196
|
}
|
|
@@ -9043,7 +9643,7 @@ const IntradayChartStyles = `
|
|
|
9043
9643
|
margin: 0 auto;
|
|
9044
9644
|
}
|
|
9045
9645
|
|
|
9046
|
-
.chart-header {
|
|
9646
|
+
.intraday-chart-widget .chart-header {
|
|
9047
9647
|
display: flex;
|
|
9048
9648
|
justify-content: space-between;
|
|
9049
9649
|
align-items: center;
|
|
@@ -9052,13 +9652,13 @@ const IntradayChartStyles = `
|
|
|
9052
9652
|
border-bottom: 1px solid #e5e7eb;
|
|
9053
9653
|
}
|
|
9054
9654
|
|
|
9055
|
-
.chart-title-section {
|
|
9655
|
+
.intraday-chart-widget .chart-title-section {
|
|
9056
9656
|
display: flex;
|
|
9057
9657
|
flex-direction: column;
|
|
9058
9658
|
gap: 4px;
|
|
9059
9659
|
}
|
|
9060
9660
|
|
|
9061
|
-
.company-market-info {
|
|
9661
|
+
.intraday-chart-widget .company-market-info {
|
|
9062
9662
|
display: flex;
|
|
9063
9663
|
gap: 8px;
|
|
9064
9664
|
align-items: center;
|
|
@@ -9066,18 +9666,18 @@ const IntradayChartStyles = `
|
|
|
9066
9666
|
color: #6b7280;
|
|
9067
9667
|
}
|
|
9068
9668
|
|
|
9069
|
-
.intraday-company-name {
|
|
9669
|
+
.intraday-chart-widget .intraday-company-name {
|
|
9070
9670
|
font-weight: 500;
|
|
9071
9671
|
}
|
|
9072
9672
|
|
|
9073
|
-
.intraday-chart-symbol {
|
|
9673
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
9074
9674
|
font-size: 1.5em;
|
|
9075
9675
|
font-weight: 700;
|
|
9076
9676
|
color: #1f2937;
|
|
9077
9677
|
margin: 0;
|
|
9078
9678
|
}
|
|
9079
9679
|
|
|
9080
|
-
.intraday-chart-source {
|
|
9680
|
+
.intraday-chart-widget .intraday-chart-source {
|
|
9081
9681
|
font-size: 0.75em;
|
|
9082
9682
|
padding: 4px 8px;
|
|
9083
9683
|
border-radius: 4px;
|
|
@@ -9086,36 +9686,36 @@ const IntradayChartStyles = `
|
|
|
9086
9686
|
font-weight: 600;
|
|
9087
9687
|
}
|
|
9088
9688
|
|
|
9089
|
-
.chart-change {
|
|
9689
|
+
.intraday-chart-widget .chart-change {
|
|
9090
9690
|
font-size: 1.1em;
|
|
9091
9691
|
font-weight: 600;
|
|
9092
9692
|
padding: 6px 12px;
|
|
9093
9693
|
border-radius: 6px;
|
|
9094
9694
|
}
|
|
9095
9695
|
|
|
9096
|
-
.chart-change.positive {
|
|
9696
|
+
.intraday-chart-widget .chart-change.positive {
|
|
9097
9697
|
color: #059669;
|
|
9098
9698
|
background: #d1fae5;
|
|
9099
9699
|
}
|
|
9100
9700
|
|
|
9101
|
-
.chart-change.negative {
|
|
9701
|
+
.intraday-chart-widget .chart-change.negative {
|
|
9102
9702
|
color: #dc2626;
|
|
9103
9703
|
background: #fee2e2;
|
|
9104
9704
|
}
|
|
9105
9705
|
|
|
9106
|
-
.chart-controls {
|
|
9706
|
+
.intraday-chart-widget .chart-controls {
|
|
9107
9707
|
display: flex;
|
|
9108
9708
|
justify-content: space-between;
|
|
9109
9709
|
align-items: center;
|
|
9110
9710
|
margin-bottom: 15px;
|
|
9111
9711
|
}
|
|
9112
9712
|
|
|
9113
|
-
.chart-range-selector {
|
|
9713
|
+
.intraday-chart-widget .chart-range-selector {
|
|
9114
9714
|
display: flex;
|
|
9115
9715
|
gap: 8px;
|
|
9116
9716
|
}
|
|
9117
9717
|
|
|
9118
|
-
.range-btn {
|
|
9718
|
+
.intraday-chart-widget .range-btn {
|
|
9119
9719
|
padding: 8px 16px;
|
|
9120
9720
|
border: 1px solid #e5e7eb;
|
|
9121
9721
|
background: white;
|
|
@@ -9127,28 +9727,28 @@ const IntradayChartStyles = `
|
|
|
9127
9727
|
transition: all 0.2s ease;
|
|
9128
9728
|
}
|
|
9129
9729
|
|
|
9130
|
-
.range-btn:hover {
|
|
9730
|
+
.intraday-chart-widget .range-btn:hover {
|
|
9131
9731
|
background: #f9fafb;
|
|
9132
9732
|
border-color: #d1d5db;
|
|
9133
9733
|
}
|
|
9134
9734
|
|
|
9135
|
-
.range-btn.active {
|
|
9735
|
+
.intraday-chart-widget .range-btn.active {
|
|
9136
9736
|
background: #667eea;
|
|
9137
9737
|
color: white;
|
|
9138
9738
|
border-color: #667eea;
|
|
9139
9739
|
}
|
|
9140
9740
|
|
|
9141
|
-
.range-btn:focus {
|
|
9741
|
+
.intraday-chart-widget .range-btn:focus {
|
|
9142
9742
|
outline: none;
|
|
9143
9743
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
9144
9744
|
}
|
|
9145
9745
|
|
|
9146
|
-
.chart-type-selector {
|
|
9746
|
+
.intraday-chart-widget .chart-type-selector {
|
|
9147
9747
|
display: flex;
|
|
9148
9748
|
gap: 8px;
|
|
9149
9749
|
}
|
|
9150
9750
|
|
|
9151
|
-
.type-btn {
|
|
9751
|
+
.intraday-chart-widget .type-btn {
|
|
9152
9752
|
padding: 8px 16px;
|
|
9153
9753
|
border: 1px solid #e5e7eb;
|
|
9154
9754
|
background: white;
|
|
@@ -9160,23 +9760,23 @@ const IntradayChartStyles = `
|
|
|
9160
9760
|
transition: all 0.2s ease;
|
|
9161
9761
|
}
|
|
9162
9762
|
|
|
9163
|
-
.type-btn:hover {
|
|
9763
|
+
.intraday-chart-widget .type-btn:hover {
|
|
9164
9764
|
background: #f9fafb;
|
|
9165
9765
|
border-color: #d1d5db;
|
|
9166
9766
|
}
|
|
9167
9767
|
|
|
9168
|
-
.type-btn.active {
|
|
9768
|
+
.intraday-chart-widget .type-btn.active {
|
|
9169
9769
|
background: #10b981;
|
|
9170
9770
|
color: white;
|
|
9171
9771
|
border-color: #10b981;
|
|
9172
9772
|
}
|
|
9173
9773
|
|
|
9174
|
-
.type-btn:focus {
|
|
9774
|
+
.intraday-chart-widget .type-btn:focus {
|
|
9175
9775
|
outline: none;
|
|
9176
9776
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
|
9177
9777
|
}
|
|
9178
9778
|
|
|
9179
|
-
.zoom-reset-btn {
|
|
9779
|
+
.intraday-chart-widget .zoom-reset-btn {
|
|
9180
9780
|
display: flex;
|
|
9181
9781
|
align-items: center;
|
|
9182
9782
|
gap: 6px;
|
|
@@ -9191,34 +9791,34 @@ const IntradayChartStyles = `
|
|
|
9191
9791
|
transition: all 0.2s ease;
|
|
9192
9792
|
}
|
|
9193
9793
|
|
|
9194
|
-
.zoom-reset-btn:hover {
|
|
9794
|
+
.intraday-chart-widget .zoom-reset-btn:hover {
|
|
9195
9795
|
background: #f9fafb;
|
|
9196
9796
|
border-color: #667eea;
|
|
9197
9797
|
color: #667eea;
|
|
9198
9798
|
}
|
|
9199
9799
|
|
|
9200
|
-
.zoom-reset-btn:focus {
|
|
9800
|
+
.intraday-chart-widget .zoom-reset-btn:focus {
|
|
9201
9801
|
outline: none;
|
|
9202
9802
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
9203
9803
|
}
|
|
9204
9804
|
|
|
9205
|
-
.zoom-reset-btn svg {
|
|
9805
|
+
.intraday-chart-widget .zoom-reset-btn svg {
|
|
9206
9806
|
flex-shrink: 0;
|
|
9207
9807
|
}
|
|
9208
9808
|
|
|
9209
|
-
.chart-container {
|
|
9809
|
+
.intraday-chart-widget .chart-container {
|
|
9210
9810
|
height: 500px;
|
|
9211
9811
|
margin-bottom: 20px;
|
|
9212
9812
|
position: relative;
|
|
9213
9813
|
}
|
|
9214
9814
|
|
|
9215
|
-
.chart-stats {
|
|
9815
|
+
.intraday-chart-widget .chart-stats {
|
|
9216
9816
|
padding: 15px;
|
|
9217
9817
|
background: #f9fafb;
|
|
9218
9818
|
border-radius: 8px;
|
|
9219
9819
|
}
|
|
9220
9820
|
|
|
9221
|
-
.stats-header {
|
|
9821
|
+
.intraday-chart-widget .stats-header {
|
|
9222
9822
|
font-size: 0.875em;
|
|
9223
9823
|
font-weight: 700;
|
|
9224
9824
|
color: #374151;
|
|
@@ -9229,19 +9829,19 @@ const IntradayChartStyles = `
|
|
|
9229
9829
|
border-bottom: 2px solid #e5e7eb;
|
|
9230
9830
|
}
|
|
9231
9831
|
|
|
9232
|
-
.stats-grid {
|
|
9832
|
+
.intraday-chart-widget .stats-grid {
|
|
9233
9833
|
display: grid;
|
|
9234
9834
|
grid-template-columns: repeat(5, 1fr);
|
|
9235
9835
|
gap: 15px;
|
|
9236
9836
|
}
|
|
9237
9837
|
|
|
9238
|
-
.stat-item {
|
|
9838
|
+
.intraday-chart-widget .stat-item {
|
|
9239
9839
|
display: flex;
|
|
9240
9840
|
flex-direction: column;
|
|
9241
9841
|
gap: 4px;
|
|
9242
9842
|
}
|
|
9243
9843
|
|
|
9244
|
-
.stat-label {
|
|
9844
|
+
.intraday-chart-widget .stat-label {
|
|
9245
9845
|
font-size: 0.75em;
|
|
9246
9846
|
color: #6b7280;
|
|
9247
9847
|
font-weight: 600;
|
|
@@ -9249,13 +9849,13 @@ const IntradayChartStyles = `
|
|
|
9249
9849
|
letter-spacing: 0.5px;
|
|
9250
9850
|
}
|
|
9251
9851
|
|
|
9252
|
-
.stat-value {
|
|
9852
|
+
.intraday-chart-widget .stat-value {
|
|
9253
9853
|
font-size: 1.1em;
|
|
9254
9854
|
font-weight: 700;
|
|
9255
9855
|
color: #1f2937;
|
|
9256
9856
|
}
|
|
9257
9857
|
|
|
9258
|
-
.widget-loading-overlay {
|
|
9858
|
+
.intraday-chart-widget .widget-loading-overlay {
|
|
9259
9859
|
position: absolute;
|
|
9260
9860
|
top: 0;
|
|
9261
9861
|
left: 0;
|
|
@@ -9274,26 +9874,26 @@ const IntradayChartStyles = `
|
|
|
9274
9874
|
backdrop-filter: blur(1px);
|
|
9275
9875
|
}
|
|
9276
9876
|
|
|
9277
|
-
.widget-loading-overlay.hidden {
|
|
9877
|
+
.intraday-chart-widget .widget-loading-overlay.hidden {
|
|
9278
9878
|
display: none;
|
|
9279
9879
|
}
|
|
9280
9880
|
|
|
9281
|
-
.loading-spinner {
|
|
9881
|
+
.intraday-chart-widget .loading-spinner {
|
|
9282
9882
|
width: 20px;
|
|
9283
9883
|
height: 20px;
|
|
9284
9884
|
border: 3px solid #e5e7eb;
|
|
9285
9885
|
border-top-color: #667eea;
|
|
9286
9886
|
border-radius: 50%;
|
|
9287
|
-
animation: spin 0.8s linear infinite;
|
|
9887
|
+
animation: intraday-spin 0.8s linear infinite;
|
|
9288
9888
|
}
|
|
9289
9889
|
|
|
9290
|
-
.loading-text {
|
|
9890
|
+
.intraday-chart-widget .loading-text {
|
|
9291
9891
|
color: #6b7280;
|
|
9292
9892
|
font-size: 0.875em;
|
|
9293
9893
|
font-weight: 500;
|
|
9294
9894
|
}
|
|
9295
9895
|
|
|
9296
|
-
@keyframes spin {
|
|
9896
|
+
@keyframes intraday-spin {
|
|
9297
9897
|
to { transform: rotate(360deg); }
|
|
9298
9898
|
}
|
|
9299
9899
|
|
|
@@ -9303,26 +9903,26 @@ const IntradayChartStyles = `
|
|
|
9303
9903
|
padding: 15px;
|
|
9304
9904
|
}
|
|
9305
9905
|
|
|
9306
|
-
.stats-grid {
|
|
9906
|
+
.intraday-chart-widget .stats-grid {
|
|
9307
9907
|
grid-template-columns: repeat(3, 1fr);
|
|
9308
9908
|
gap: 10px;
|
|
9309
9909
|
}
|
|
9310
9910
|
|
|
9311
|
-
.stats-header {
|
|
9911
|
+
.intraday-chart-widget .stats-header {
|
|
9312
9912
|
font-size: 0.8em;
|
|
9313
9913
|
margin-bottom: 10px;
|
|
9314
9914
|
}
|
|
9315
9915
|
|
|
9316
|
-
.chart-container {
|
|
9916
|
+
.intraday-chart-widget .chart-container {
|
|
9317
9917
|
height: 350px;
|
|
9318
9918
|
}
|
|
9319
9919
|
|
|
9320
|
-
.intraday-chart-symbol {
|
|
9920
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
9321
9921
|
font-size: 1.2em;
|
|
9322
9922
|
}
|
|
9323
9923
|
}
|
|
9324
9924
|
|
|
9325
|
-
.widget-error {
|
|
9925
|
+
.intraday-chart-widget .widget-error {
|
|
9326
9926
|
padding: 15px;
|
|
9327
9927
|
background: #fee2e2;
|
|
9328
9928
|
border: 1px solid #fecaca;
|
|
@@ -38698,7 +39298,20 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
38698
39298
|
}
|
|
38699
39299
|
async initialize() {
|
|
38700
39300
|
// Validate initial symbol via API to get company info
|
|
38701
|
-
|
|
39301
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
39302
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
39303
|
+
// Extract company info from first data item
|
|
39304
|
+
if (data && data[0]) {
|
|
39305
|
+
this.companyName = data[0].comp_name || '';
|
|
39306
|
+
this.exchangeName = data[0].market_name || '';
|
|
39307
|
+
this.mic = data[0].mic || '';
|
|
39308
|
+
}
|
|
39309
|
+
});
|
|
39310
|
+
|
|
39311
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
39312
|
+
if (validationSuccess === false) {
|
|
39313
|
+
return; // Error is already shown, don't continue
|
|
39314
|
+
}
|
|
38702
39315
|
|
|
38703
39316
|
// Update company name in the header
|
|
38704
39317
|
this.updateCompanyName();
|
|
@@ -38706,38 +39319,6 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
38706
39319
|
// Load chart data
|
|
38707
39320
|
await this.loadChartData();
|
|
38708
39321
|
}
|
|
38709
|
-
async validateInitialSymbol() {
|
|
38710
|
-
try {
|
|
38711
|
-
const apiService = this.wsManager.getApiService();
|
|
38712
|
-
if (!apiService) {
|
|
38713
|
-
if (this.debug) {
|
|
38714
|
-
console.log('[IntradayChartWidget] API service not available for initial validation');
|
|
38715
|
-
}
|
|
38716
|
-
return;
|
|
38717
|
-
}
|
|
38718
|
-
const result = await apiService.quotel1(this.symbol);
|
|
38719
|
-
if (result && result.data && result.data[0]) {
|
|
38720
|
-
const symbolData = result.data[0];
|
|
38721
|
-
// Extract company info
|
|
38722
|
-
this.companyName = symbolData.comp_name || '';
|
|
38723
|
-
this.exchangeName = symbolData.market_name || '';
|
|
38724
|
-
this.mic = symbolData.mic || '';
|
|
38725
|
-
if (this.debug) {
|
|
38726
|
-
console.log('[IntradayChartWidget] Initial symbol validated:', {
|
|
38727
|
-
symbol: this.symbol,
|
|
38728
|
-
companyName: this.companyName,
|
|
38729
|
-
exchangeName: this.exchangeName,
|
|
38730
|
-
mic: this.mic
|
|
38731
|
-
});
|
|
38732
|
-
}
|
|
38733
|
-
}
|
|
38734
|
-
} catch (error) {
|
|
38735
|
-
if (this.debug) {
|
|
38736
|
-
console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
|
|
38737
|
-
}
|
|
38738
|
-
// Don't throw - let the widget continue with chart data
|
|
38739
|
-
}
|
|
38740
|
-
}
|
|
38741
39322
|
updateCompanyName() {
|
|
38742
39323
|
const companyNameElement = this.container.querySelector('.intraday-company-name');
|
|
38743
39324
|
if (companyNameElement) {
|
|
@@ -39161,19 +39742,12 @@ class IntradayChartWidget extends BaseWidget {
|
|
|
39161
39742
|
// Handle array format (standard night session format)
|
|
39162
39743
|
if (Array.isArray(message.data)) {
|
|
39163
39744
|
console.log('[IntradayChartWidget] Processing array format, length:', message.length);
|
|
39164
|
-
const symbolData = message.find(item => item.Symbol === this.symbol);
|
|
39745
|
+
const symbolData = message.data.find(item => item.Symbol === this.symbol);
|
|
39165
39746
|
console.log('[IntradayChartWidget] Found symbol data:', symbolData);
|
|
39166
39747
|
if (symbolData && !symbolData.NotFound) {
|
|
39167
39748
|
priceData = symbolData;
|
|
39168
39749
|
}
|
|
39169
39750
|
}
|
|
39170
|
-
// Handle wrapped format
|
|
39171
|
-
else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
|
|
39172
|
-
console.log('[IntradayChartWidget] Processing wrapped format');
|
|
39173
|
-
if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
|
|
39174
|
-
priceData = message['0'];
|
|
39175
|
-
}
|
|
39176
|
-
}
|
|
39177
39751
|
// Handle direct data format
|
|
39178
39752
|
else if (message.Symbol === this.symbol && !message.NotFound) {
|
|
39179
39753
|
console.log('[IntradayChartWidget] Processing direct format');
|
|
@@ -40276,7 +40850,19 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40276
40850
|
this.showLoading();
|
|
40277
40851
|
|
|
40278
40852
|
// Fetch company info for initial symbol
|
|
40279
|
-
|
|
40853
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
40854
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
40855
|
+
// Extract company info from first data item
|
|
40856
|
+
if (data && data[0]) {
|
|
40857
|
+
this.companyName = data[0].comp_name || '';
|
|
40858
|
+
this.exchangeName = data[0].market_name || '';
|
|
40859
|
+
}
|
|
40860
|
+
});
|
|
40861
|
+
|
|
40862
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
40863
|
+
if (validationSuccess === false) {
|
|
40864
|
+
return; // Error is already shown, don't continue
|
|
40865
|
+
}
|
|
40280
40866
|
|
|
40281
40867
|
// Set timeout to detect no data on initial load
|
|
40282
40868
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -40288,36 +40874,6 @@ class ONBBOLevel2Widget extends BaseWidget {
|
|
|
40288
40874
|
}, 10000);
|
|
40289
40875
|
this.subscribeToData();
|
|
40290
40876
|
}
|
|
40291
|
-
async validateInitialSymbol() {
|
|
40292
|
-
try {
|
|
40293
|
-
const apiService = this.wsManager.getApiService();
|
|
40294
|
-
if (!apiService) {
|
|
40295
|
-
if (this.debug) {
|
|
40296
|
-
console.log('[ONBBO L2] API service not available for initial validation');
|
|
40297
|
-
}
|
|
40298
|
-
return;
|
|
40299
|
-
}
|
|
40300
|
-
const result = await apiService.quotel1(this.symbol);
|
|
40301
|
-
if (result && result.data && result.data[0]) {
|
|
40302
|
-
const symbolData = result.data[0];
|
|
40303
|
-
// Extract company info
|
|
40304
|
-
this.companyName = symbolData.comp_name || '';
|
|
40305
|
-
this.exchangeName = symbolData.market_name || '';
|
|
40306
|
-
if (this.debug) {
|
|
40307
|
-
console.log('[ONBBO L2] Initial symbol validated:', {
|
|
40308
|
-
symbol: this.symbol,
|
|
40309
|
-
companyName: this.companyName,
|
|
40310
|
-
exchangeName: this.exchangeName
|
|
40311
|
-
});
|
|
40312
|
-
}
|
|
40313
|
-
}
|
|
40314
|
-
} catch (error) {
|
|
40315
|
-
if (this.debug) {
|
|
40316
|
-
console.warn('[ONBBO L2] Initial symbol validation failed:', error);
|
|
40317
|
-
}
|
|
40318
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
40319
|
-
}
|
|
40320
|
-
}
|
|
40321
40877
|
subscribeToData() {
|
|
40322
40878
|
// Subscribe to ONBBO Level 2 data (order book)
|
|
40323
40879
|
// Use separate widget IDs to avoid "already active" duplicate detection
|
|
@@ -40836,6 +41392,7 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40836
41392
|
this.styled = options.styled !== undefined ? options.styled : true;
|
|
40837
41393
|
this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
|
|
40838
41394
|
this.data = new TimeSalesModel([], this.maxTrades);
|
|
41395
|
+
this.data.symbol = this.symbol; // Set the symbol in the data model
|
|
40839
41396
|
this.isDestroyed = false;
|
|
40840
41397
|
this.unsubscribe = null;
|
|
40841
41398
|
this.symbolEditor = null;
|
|
@@ -40847,6 +41404,7 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40847
41404
|
|
|
40848
41405
|
// Create widget structure
|
|
40849
41406
|
this.createWidgetStructure();
|
|
41407
|
+
this.initializeSymbolEditor();
|
|
40850
41408
|
|
|
40851
41409
|
// Initialize the widget
|
|
40852
41410
|
this.initialize();
|
|
@@ -40863,7 +41421,26 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40863
41421
|
}
|
|
40864
41422
|
}
|
|
40865
41423
|
this.addStyles();
|
|
40866
|
-
this.setupSymbolEditor();
|
|
41424
|
+
//this.setupSymbolEditor();
|
|
41425
|
+
}
|
|
41426
|
+
initializeSymbolEditor() {
|
|
41427
|
+
// Initialize symbol editor with stock symbol validation
|
|
41428
|
+
this.symbolEditor = new SymbolEditor(this, {
|
|
41429
|
+
maxLength: 10,
|
|
41430
|
+
placeholder: 'Enter symbol...',
|
|
41431
|
+
//validator: this.validateStockSymbol.bind(this),
|
|
41432
|
+
onSymbolChange: this.handleSymbolChange.bind(this),
|
|
41433
|
+
debug: this.debug,
|
|
41434
|
+
autoUppercase: true,
|
|
41435
|
+
symbolType: 'quotel1'
|
|
41436
|
+
});
|
|
41437
|
+
|
|
41438
|
+
// Set initial symbol
|
|
41439
|
+
const symbolElement = this.container.querySelector('.symbol');
|
|
41440
|
+
if (symbolElement) {
|
|
41441
|
+
symbolElement.textContent = this.symbol;
|
|
41442
|
+
symbolElement.dataset.originalSymbol = this.symbol;
|
|
41443
|
+
}
|
|
40867
41444
|
}
|
|
40868
41445
|
addStyles() {
|
|
40869
41446
|
// Inject styles from the styles file into document head
|
|
@@ -40886,6 +41463,8 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40886
41463
|
// Keep editor open when clicking buttons
|
|
40887
41464
|
symbolType: 'quotel1' // Use standard quote validation
|
|
40888
41465
|
});
|
|
41466
|
+
|
|
41467
|
+
//console.log('SETUP COMPLETE')
|
|
40889
41468
|
}
|
|
40890
41469
|
async handleSymbolChange(newSymbol, oldSymbol) {
|
|
40891
41470
|
let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
@@ -40988,7 +41567,19 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
40988
41567
|
this.showLoading();
|
|
40989
41568
|
|
|
40990
41569
|
// Validate initial symbol via API to get company info
|
|
40991
|
-
|
|
41570
|
+
// Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
|
|
41571
|
+
const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
|
|
41572
|
+
// Extract company info from first data item
|
|
41573
|
+
if (data && data[0]) {
|
|
41574
|
+
this.companyName = data[0].comp_name || '';
|
|
41575
|
+
this.exchangeName = data[0].market_name || '';
|
|
41576
|
+
}
|
|
41577
|
+
});
|
|
41578
|
+
|
|
41579
|
+
// If validation failed due to access/permission issues, stop initialization
|
|
41580
|
+
if (validationSuccess === false) {
|
|
41581
|
+
return; // Error is already shown, don't continue
|
|
41582
|
+
}
|
|
40992
41583
|
|
|
40993
41584
|
// Set timeout to detect no data on initial load
|
|
40994
41585
|
this.loadingTimeout = setTimeout(() => {
|
|
@@ -41001,36 +41592,6 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
41001
41592
|
|
|
41002
41593
|
this.subscribeToData();
|
|
41003
41594
|
}
|
|
41004
|
-
async validateInitialSymbol() {
|
|
41005
|
-
try {
|
|
41006
|
-
const apiService = this.wsManager.getApiService();
|
|
41007
|
-
if (!apiService) {
|
|
41008
|
-
if (this.debug) {
|
|
41009
|
-
console.log('[TimeSalesWidget] API service not available for initial validation');
|
|
41010
|
-
}
|
|
41011
|
-
return;
|
|
41012
|
-
}
|
|
41013
|
-
const result = await apiService.quotel1(this.symbol);
|
|
41014
|
-
if (result && result.data && result.data[0]) {
|
|
41015
|
-
const symbolData = result.data[0];
|
|
41016
|
-
// Extract company info
|
|
41017
|
-
this.companyName = symbolData.comp_name || '';
|
|
41018
|
-
this.exchangeName = symbolData.market_name || '';
|
|
41019
|
-
if (this.debug) {
|
|
41020
|
-
console.log('[TimeSalesWidget] Initial symbol validated:', {
|
|
41021
|
-
symbol: this.symbol,
|
|
41022
|
-
companyName: this.companyName,
|
|
41023
|
-
exchangeName: this.exchangeName
|
|
41024
|
-
});
|
|
41025
|
-
}
|
|
41026
|
-
}
|
|
41027
|
-
} catch (error) {
|
|
41028
|
-
if (this.debug) {
|
|
41029
|
-
console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
|
|
41030
|
-
}
|
|
41031
|
-
// Don't throw - let the widget continue with WebSocket data
|
|
41032
|
-
}
|
|
41033
|
-
}
|
|
41034
41595
|
subscribeToData() {
|
|
41035
41596
|
// Subscribe to Time & Sales data (using queryonbbotimesale)
|
|
41036
41597
|
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
|
|
@@ -41046,6 +41607,9 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
41046
41607
|
clearTimeout(this.loadingTimeout);
|
|
41047
41608
|
this.loadingTimeout = null;
|
|
41048
41609
|
}
|
|
41610
|
+
if (message.type != 'queryonbbotimesale') {
|
|
41611
|
+
return;
|
|
41612
|
+
}
|
|
41049
41613
|
|
|
41050
41614
|
// Handle error messages
|
|
41051
41615
|
if (message.type === 'error') {
|
|
@@ -41058,30 +41622,16 @@ class TimeSalesWidget extends BaseWidget {
|
|
|
41058
41622
|
}
|
|
41059
41623
|
|
|
41060
41624
|
// Handle Time & Sales data - Array of trades
|
|
41061
|
-
if (Array.isArray(message)) {
|
|
41625
|
+
if (Array.isArray(message.data)) {
|
|
41062
41626
|
if (this.debug) {
|
|
41063
41627
|
console.log('[TimeSalesWidget] Received trades array:', message);
|
|
41064
41628
|
}
|
|
41065
41629
|
|
|
41066
41630
|
// Add new trades to existing data
|
|
41067
|
-
this.data.addTrades(message);
|
|
41631
|
+
this.data.addTrades(message.data);
|
|
41068
41632
|
this.updateWidget();
|
|
41069
41633
|
return;
|
|
41070
41634
|
}
|
|
41071
|
-
|
|
41072
|
-
// Handle wrapped format
|
|
41073
|
-
if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
|
|
41074
|
-
if (Array.isArray(message['0'])) {
|
|
41075
|
-
if (this.debug) {
|
|
41076
|
-
console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
|
|
41077
|
-
}
|
|
41078
|
-
|
|
41079
|
-
// Add new trades to existing data
|
|
41080
|
-
this.data.addTrades(message['0']);
|
|
41081
|
-
this.updateWidget();
|
|
41082
|
-
return;
|
|
41083
|
-
}
|
|
41084
|
-
}
|
|
41085
41635
|
if (this.debug) {
|
|
41086
41636
|
console.log('[TimeSalesWidget] Unexpected message format:', message);
|
|
41087
41637
|
}
|
|
@@ -41308,7 +41858,15 @@ class ApiService {
|
|
|
41308
41858
|
try {
|
|
41309
41859
|
const response = await fetch(url, config);
|
|
41310
41860
|
if (!response.ok) {
|
|
41311
|
-
|
|
41861
|
+
// Try to get response body for error details
|
|
41862
|
+
let errorBody = '';
|
|
41863
|
+
try {
|
|
41864
|
+
errorBody = await response.text();
|
|
41865
|
+
console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
|
|
41866
|
+
} catch (e) {
|
|
41867
|
+
console.error(`[ApiService] Could not read error response body:`, e);
|
|
41868
|
+
}
|
|
41869
|
+
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
|
|
41312
41870
|
}
|
|
41313
41871
|
return await response.json();
|
|
41314
41872
|
} catch (error) {
|
|
@@ -42068,8 +42626,8 @@ class WebSocketManager {
|
|
|
42068
42626
|
*/
|
|
42069
42627
|
_normalizeMessage(message) {
|
|
42070
42628
|
// Check for error field (case-insensitive)
|
|
42071
|
-
const errorField = message.error
|
|
42072
|
-
const typeField = message.type
|
|
42629
|
+
const errorField = message.error;
|
|
42630
|
+
const typeField = message.type;
|
|
42073
42631
|
const messageField = message.message || message.Message;
|
|
42074
42632
|
|
|
42075
42633
|
// Handle explicit error messages - route based on type field
|
|
@@ -42109,54 +42667,16 @@ class WebSocketManager {
|
|
|
42109
42667
|
}
|
|
42110
42668
|
}
|
|
42111
42669
|
|
|
42112
|
-
// Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
|
|
42113
|
-
const dataField = message.data || message.Data;
|
|
42114
|
-
if (typeField && dataField !== undefined) {
|
|
42115
|
-
if (this.config.debug) {
|
|
42116
|
-
console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
|
|
42117
|
-
}
|
|
42118
|
-
|
|
42119
|
-
// Extract the actual data and add the type to it for routing
|
|
42120
|
-
let normalizedData;
|
|
42121
|
-
if (Array.isArray(dataField)) {
|
|
42122
|
-
// If data is an array, process each item
|
|
42123
|
-
normalizedData = dataField;
|
|
42124
|
-
// Add type info to the structure for routing
|
|
42125
|
-
if (normalizedData.length > 0) {
|
|
42126
|
-
normalizedData._messageType = typeField;
|
|
42127
|
-
}
|
|
42128
|
-
} else if (typeof dataField === 'object' && dataField !== null) {
|
|
42129
|
-
// If data is an object, use it directly
|
|
42130
|
-
normalizedData = dataField;
|
|
42131
|
-
// Add type info for routing
|
|
42132
|
-
normalizedData._messageType = typeField;
|
|
42133
|
-
} else {
|
|
42134
|
-
// Primitive value, wrap it
|
|
42135
|
-
normalizedData = {
|
|
42136
|
-
value: dataField,
|
|
42137
|
-
_messageType: typeField
|
|
42138
|
-
};
|
|
42139
|
-
}
|
|
42140
|
-
|
|
42141
|
-
// Only add type field if the normalized data doesn't already have one
|
|
42142
|
-
if (typeof normalizedData === 'object' && !normalizedData.type && !normalizedData.Type) {
|
|
42143
|
-
normalizedData.type = typeField;
|
|
42144
|
-
}
|
|
42145
|
-
return normalizedData;
|
|
42146
|
-
}
|
|
42147
|
-
|
|
42148
42670
|
// Old format or no type/data structure, return as is
|
|
42149
42671
|
return message;
|
|
42150
42672
|
}
|
|
42151
42673
|
_routeMessage(message) {
|
|
42152
42674
|
//console.log('message', message);
|
|
42153
42675
|
|
|
42154
|
-
|
|
42155
|
-
|
|
42156
|
-
|
|
42157
|
-
|
|
42158
|
-
return; // Don't route empty arrays
|
|
42159
|
-
}
|
|
42676
|
+
// IMPORTANT: Don't filter out empty arrays here - route them based on message.type
|
|
42677
|
+
// Widgets need to receive empty arrays to clear loading states and show "no data" messages
|
|
42678
|
+
// The message structure is: { type: 'queryoptionchain', data: [] }
|
|
42679
|
+
// We need to route based on the 'type' field, not the data content
|
|
42160
42680
|
|
|
42161
42681
|
// Cache the message for later use by new subscribers
|
|
42162
42682
|
this._cacheMessage(message);
|
|
@@ -42255,27 +42775,34 @@ class WebSocketManager {
|
|
|
42255
42775
|
_getRelevantWidgets(message) {
|
|
42256
42776
|
const relevantWidgets = new Set();
|
|
42257
42777
|
|
|
42778
|
+
// OPTIMIZATION: Extract message type once from the entire message, not from each data item
|
|
42779
|
+
// Message structure: { type: 'queryoptionchain', data: [] }
|
|
42780
|
+
let dataField = message.Data || message.data;
|
|
42781
|
+
const messageType = message.type || this._extractMessageType(dataField);
|
|
42782
|
+
|
|
42258
42783
|
// Handle array messages
|
|
42259
|
-
this._addRelevantWidgetsForItem(message, relevantWidgets);
|
|
42784
|
+
this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
|
|
42260
42785
|
return relevantWidgets;
|
|
42261
42786
|
}
|
|
42262
|
-
_addRelevantWidgetsForItem(item, relevantWidgets) {
|
|
42787
|
+
_addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
|
|
42263
42788
|
// If item has Data array, process it
|
|
42264
42789
|
if (item.Data && Array.isArray(item.Data)) {
|
|
42265
42790
|
//console.log('Data array found with length:', item.Data.length);
|
|
42266
42791
|
item.Data.forEach(dataItem => {
|
|
42267
|
-
|
|
42792
|
+
// Pass messageType from parent message, don't extract from each data item
|
|
42793
|
+
this._processDataItem(dataItem, relevantWidgets, messageType);
|
|
42268
42794
|
});
|
|
42269
|
-
} else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
|
|
42270
|
-
this._processOptionChainData(item, relevantWidgets);
|
|
42795
|
+
} else if (item.data && item.data[0] && item.data[0].Strike !== undefined && item.data[0].Expire && !item.data[0].underlyingSymbol) {
|
|
42796
|
+
this._processOptionChainData(item.data, relevantWidgets);
|
|
42271
42797
|
} else {
|
|
42272
|
-
// Process single item
|
|
42273
|
-
this._processDataItem(item, relevantWidgets);
|
|
42798
|
+
// Process single item - messageType already extracted from message
|
|
42799
|
+
this._processDataItem(item, relevantWidgets, messageType);
|
|
42274
42800
|
}
|
|
42275
42801
|
}
|
|
42276
42802
|
|
|
42277
42803
|
// Simplified option chain processing - handle entire array at once
|
|
42278
42804
|
_processOptionChainData(optionChainArray, relevantWidgets) {
|
|
42805
|
+
//console.log('PROCESSING DATA OPTIONS')
|
|
42279
42806
|
if (!optionChainArray || optionChainArray.length === 0) return;
|
|
42280
42807
|
// Extract underlying symbol and date from first option contract
|
|
42281
42808
|
const firstOption = optionChainArray[0];
|
|
@@ -42314,12 +42841,11 @@ class WebSocketManager {
|
|
|
42314
42841
|
}
|
|
42315
42842
|
}
|
|
42316
42843
|
}
|
|
42317
|
-
_processDataItem(dataItem, relevantWidgets) {
|
|
42844
|
+
_processDataItem(dataItem, relevantWidgets, messageType) {
|
|
42318
42845
|
// Process individual data items and route to appropriate widgets
|
|
42319
42846
|
// Option chain arrays are handled separately by _processOptionChainData
|
|
42320
42847
|
|
|
42321
42848
|
const symbol = this._extractSymbol(dataItem);
|
|
42322
|
-
const messageType = this._extractMessageType(dataItem);
|
|
42323
42849
|
if (this.config.debug) {
|
|
42324
42850
|
console.log('[WebSocketManager] Processing data item:', {
|
|
42325
42851
|
symbol,
|
|
@@ -42479,25 +43005,15 @@ class WebSocketManager {
|
|
|
42479
43005
|
return symbolMappings[extracted] || extracted;
|
|
42480
43006
|
}
|
|
42481
43007
|
_extractMessageType(item) {
|
|
42482
|
-
//
|
|
42483
|
-
if (item._messageType) {
|
|
42484
|
-
return item._messageType;
|
|
42485
|
-
}
|
|
42486
|
-
|
|
42487
|
-
// Determine message type based on content
|
|
42488
|
-
if (item.type) return item.type;
|
|
42489
|
-
|
|
42490
|
-
// Infer type from data structure
|
|
43008
|
+
// FALLBACK: Infer type from data structure (for legacy/malformed messages)
|
|
42491
43009
|
|
|
42492
43010
|
// IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
|
|
42493
43011
|
// Level 2 ONBBO data - array of MMID objects or object with MMID field
|
|
42494
43012
|
if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
|
|
42495
43013
|
return 'queryonbbol2';
|
|
42496
43014
|
}
|
|
42497
|
-
|
|
42498
|
-
|
|
42499
|
-
if (item.MMID !== undefined) {
|
|
42500
|
-
return 'queryonbbol2';
|
|
43015
|
+
if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
|
|
43016
|
+
return 'queryoptionchain';
|
|
42501
43017
|
}
|
|
42502
43018
|
|
|
42503
43019
|
// Check for Source field AFTER MMID check
|