mdas-jsview-sdk 1.0.23-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 +1004 -230
- package/dist/mdas-sdk.esm.js.map +1 -1
- package/dist/mdas-sdk.js +1004 -230
- 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
|
@@ -2857,8 +2857,36 @@ const TimeSalesStyles = `
|
|
|
2857
2857
|
}
|
|
2858
2858
|
`;
|
|
2859
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
|
+
*/
|
|
2860
2887
|
const SharedStyles = `
|
|
2861
2888
|
/* ========================================
|
|
2889
|
+
⚠️ DEPRECATED - Use BaseStyles.js instead
|
|
2862
2890
|
FONT SIZE SYSTEM - CSS Variables
|
|
2863
2891
|
Adjust --mdas-base-font-size to scale all fonts
|
|
2864
2892
|
======================================== */
|
|
@@ -6638,6 +6666,15 @@ const OptionChainTemplate = `
|
|
|
6638
6666
|
</select>
|
|
6639
6667
|
<button class="fetch-button" disabled>Search</button>
|
|
6640
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>
|
|
6641
6678
|
</div>
|
|
6642
6679
|
|
|
6643
6680
|
<!-- Data Grid Section -->
|
|
@@ -6694,11 +6731,383 @@ const OptionChainTemplate = `
|
|
|
6694
6731
|
</div>
|
|
6695
6732
|
`;
|
|
6696
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
|
+
|
|
6697
7102
|
// src/widgets/styles/OptionChainStyles.js
|
|
6698
7103
|
const OptionChainStyles = `
|
|
6699
|
-
${
|
|
7104
|
+
${BaseStyles}
|
|
7105
|
+
${getLoadingOverlayStyles('option-chain-widget')}
|
|
7106
|
+
${getErrorStyles('option-chain-widget')}
|
|
7107
|
+
${getFooterStyles('option-chain-widget')}
|
|
7108
|
+
${getNoDataStyles('option-chain-widget')}
|
|
6700
7109
|
|
|
6701
|
-
/*
|
|
7110
|
+
/* Option Chain Widget Specific Styles */
|
|
6702
7111
|
.option-chain-widget {
|
|
6703
7112
|
border: 1px solid #e5e7eb;
|
|
6704
7113
|
border-radius: 8px;
|
|
@@ -6723,8 +7132,22 @@ ${SharedStyles}
|
|
|
6723
7132
|
flex-wrap: wrap;
|
|
6724
7133
|
}
|
|
6725
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
|
+
|
|
6726
7148
|
.option-chain-widget .symbol-input,
|
|
6727
|
-
.option-chain-widget .date-select
|
|
7149
|
+
.option-chain-widget .date-select,
|
|
7150
|
+
.option-chain-widget .strike-filter {
|
|
6728
7151
|
padding: 8px 12px;
|
|
6729
7152
|
border: 1px solid #d1d5db;
|
|
6730
7153
|
border-radius: 4px;
|
|
@@ -6745,6 +7168,12 @@ ${SharedStyles}
|
|
|
6745
7168
|
background: white;
|
|
6746
7169
|
}
|
|
6747
7170
|
|
|
7171
|
+
.option-chain-widget .strike-filter {
|
|
7172
|
+
min-width: 200px;
|
|
7173
|
+
background: white;
|
|
7174
|
+
cursor: pointer;
|
|
7175
|
+
}
|
|
7176
|
+
|
|
6748
7177
|
.option-chain-widget .fetch-button {
|
|
6749
7178
|
padding: 8px 16px;
|
|
6750
7179
|
background: #3b82f6;
|
|
@@ -6900,10 +7329,22 @@ ${SharedStyles}
|
|
|
6900
7329
|
min-height: 32px;
|
|
6901
7330
|
}
|
|
6902
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
|
+
|
|
6903
7338
|
.option-chain-widget .option-row:hover {
|
|
6904
7339
|
background: #f9fafb;
|
|
6905
7340
|
}
|
|
6906
|
-
|
|
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,
|
|
6907
7348
|
.option-chain-widget .option-row:hover .puts-data span,
|
|
6908
7349
|
.option-chain-widget .option-row:hover .strike-data {
|
|
6909
7350
|
background: #f9fafb;
|
|
@@ -6927,14 +7368,32 @@ ${SharedStyles}
|
|
|
6927
7368
|
.option-chain-widget .puts-data span {
|
|
6928
7369
|
padding: 6px 4px;
|
|
6929
7370
|
text-align: center;
|
|
6930
|
-
font-size: 12px;
|
|
6931
|
-
font-weight: 400;
|
|
6932
|
-
color: #111827;
|
|
7371
|
+
font-size: 12px;
|
|
7372
|
+
font-weight: 400;
|
|
7373
|
+
color: #111827;
|
|
6933
7374
|
overflow: hidden;
|
|
6934
7375
|
text-overflow: ellipsis;
|
|
6935
7376
|
white-space: nowrap;
|
|
6936
7377
|
}
|
|
6937
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
|
+
|
|
6938
7397
|
.option-chain-widget .contract-cell {
|
|
6939
7398
|
font-size: 10px;
|
|
6940
7399
|
color: #6b7280;
|
|
@@ -6946,78 +7405,42 @@ ${SharedStyles}
|
|
|
6946
7405
|
white-space: nowrap;
|
|
6947
7406
|
}
|
|
6948
7407
|
|
|
6949
|
-
.option-chain-widget .
|
|
6950
|
-
color: #
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
color: #dc2626;
|
|
6955
|
-
}
|
|
6956
|
-
|
|
6957
|
-
.option-chain-widget .neutral {
|
|
6958
|
-
color: #6b7280;
|
|
6959
|
-
}
|
|
6960
|
-
|
|
6961
|
-
.option-chain-widget .widget-footer {
|
|
6962
|
-
background: #f9fafb;
|
|
6963
|
-
padding: 8px 16px;
|
|
6964
|
-
border-top: 1px solid #e5e7eb;
|
|
6965
|
-
text-align: center;
|
|
6966
|
-
font-size: 11px;
|
|
6967
|
-
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;
|
|
6968
7413
|
}
|
|
6969
7414
|
|
|
6970
|
-
.option-chain-widget .
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
left: 0;
|
|
6974
|
-
right: 0;
|
|
6975
|
-
bottom: 0;
|
|
6976
|
-
background: rgba(255, 255, 255, 0.9);
|
|
6977
|
-
display: flex;
|
|
6978
|
-
align-items: center;
|
|
6979
|
-
justify-content: center;
|
|
6980
|
-
z-index: 20;
|
|
7415
|
+
.option-chain-widget .contract-cell.clickable:hover {
|
|
7416
|
+
color: #2563eb;
|
|
7417
|
+
background-color: #eff6ff;
|
|
6981
7418
|
}
|
|
6982
7419
|
|
|
6983
|
-
.option-chain-widget .
|
|
6984
|
-
|
|
7420
|
+
.option-chain-widget .contract-cell.clickable:focus {
|
|
7421
|
+
outline: 2px solid #3b82f6;
|
|
7422
|
+
outline-offset: 2px;
|
|
7423
|
+
border-radius: 2px;
|
|
6985
7424
|
}
|
|
6986
7425
|
|
|
6987
|
-
.option-chain-widget .
|
|
6988
|
-
|
|
7426
|
+
.option-chain-widget .contract-cell.clickable:active {
|
|
7427
|
+
color: #1e40af;
|
|
7428
|
+
background-color: #dbeafe;
|
|
6989
7429
|
}
|
|
6990
7430
|
|
|
6991
|
-
.option-chain-widget .
|
|
6992
|
-
|
|
6993
|
-
height: 32px;
|
|
6994
|
-
border: 3px solid #e5e7eb;
|
|
6995
|
-
border-top: 3px solid #3b82f6;
|
|
6996
|
-
border-radius: 50%;
|
|
6997
|
-
animation: spin 1s linear infinite;
|
|
6998
|
-
margin: 0 auto 12px;
|
|
7431
|
+
.option-chain-widget .positive {
|
|
7432
|
+
color: #059669;
|
|
6999
7433
|
}
|
|
7000
7434
|
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
100% { transform: rotate(360deg); }
|
|
7435
|
+
.option-chain-widget .negative {
|
|
7436
|
+
color: #dc2626;
|
|
7004
7437
|
}
|
|
7005
7438
|
|
|
7006
|
-
.option-chain-widget .
|
|
7007
|
-
padding: 40px;
|
|
7008
|
-
text-align: center;
|
|
7439
|
+
.option-chain-widget .neutral {
|
|
7009
7440
|
color: #6b7280;
|
|
7010
7441
|
}
|
|
7011
7442
|
|
|
7012
|
-
|
|
7013
|
-
padding: 20px;
|
|
7014
|
-
background: #fef2f2;
|
|
7015
|
-
color: #dc2626;
|
|
7016
|
-
border: 1px solid #fecaca;
|
|
7017
|
-
margin: 16px;
|
|
7018
|
-
border-radius: 4px;
|
|
7019
|
-
text-align: center;
|
|
7020
|
-
}
|
|
7443
|
+
/* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
|
|
7021
7444
|
|
|
7022
7445
|
/* RESPONSIVE STYLES */
|
|
7023
7446
|
|
|
@@ -7028,13 +7451,19 @@ ${SharedStyles}
|
|
|
7028
7451
|
align-items: stretch;
|
|
7029
7452
|
gap: 8px;
|
|
7030
7453
|
}
|
|
7031
|
-
|
|
7454
|
+
|
|
7455
|
+
.option-chain-widget .filter-section {
|
|
7456
|
+
flex-direction: row;
|
|
7457
|
+
margin-top: 0;
|
|
7458
|
+
}
|
|
7459
|
+
|
|
7032
7460
|
.option-chain-widget .symbol-input,
|
|
7033
|
-
.option-chain-widget .date-select
|
|
7461
|
+
.option-chain-widget .date-select,
|
|
7462
|
+
.option-chain-widget .strike-filter {
|
|
7034
7463
|
width: 100%;
|
|
7035
7464
|
max-width: none;
|
|
7036
7465
|
}
|
|
7037
|
-
|
|
7466
|
+
|
|
7038
7467
|
.option-chain-widget .fetch-button {
|
|
7039
7468
|
width: 100%;
|
|
7040
7469
|
}
|
|
@@ -7190,6 +7619,121 @@ ${SharedStyles}
|
|
|
7190
7619
|
padding: 2px 1px;
|
|
7191
7620
|
}
|
|
7192
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
|
+
}
|
|
7193
7737
|
`;
|
|
7194
7738
|
|
|
7195
7739
|
class OptionChainWidget extends BaseWidget {
|
|
@@ -7204,7 +7748,18 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7204
7748
|
this.data = null;
|
|
7205
7749
|
this.isDestroyed = false;
|
|
7206
7750
|
this.unsubscribe = null;
|
|
7751
|
+
this.unsubscribeUnderlying = null; // For underlying stock price
|
|
7752
|
+
this.underlyingPrice = null; // Current price of underlying stock
|
|
7207
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;
|
|
7208
7763
|
|
|
7209
7764
|
// INPUT VALIDATION: Validate initial symbol if provided
|
|
7210
7765
|
if (options.symbol) {
|
|
@@ -7220,6 +7775,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7220
7775
|
}
|
|
7221
7776
|
this.date = '';
|
|
7222
7777
|
this.availableDates = {};
|
|
7778
|
+
this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
|
|
7223
7779
|
|
|
7224
7780
|
// RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
|
|
7225
7781
|
this.fetchRateLimiter = createRateLimitValidator(1000);
|
|
@@ -7250,6 +7806,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7250
7806
|
this.dateSelect = this.container.querySelector('.date-select');
|
|
7251
7807
|
this.fetchButton = this.container.querySelector('.fetch-button');
|
|
7252
7808
|
this.dataGrid = this.container.querySelector('.option-chain-data-grid');
|
|
7809
|
+
this.strikeFilterSelect = this.container.querySelector('.strike-filter');
|
|
7253
7810
|
|
|
7254
7811
|
// Set initial symbol value if provided in options
|
|
7255
7812
|
if (this.symbol) {
|
|
@@ -7324,6 +7881,22 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7324
7881
|
}
|
|
7325
7882
|
this.loadAvailableDates(symbolValidation.sanitized);
|
|
7326
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
|
+
});
|
|
7327
7900
|
}
|
|
7328
7901
|
async loadAvailableDates(symbol) {
|
|
7329
7902
|
if (!symbol) return;
|
|
@@ -7339,7 +7912,9 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7339
7912
|
// Use API service from wsManager
|
|
7340
7913
|
const apiService = this.wsManager.getApiService();
|
|
7341
7914
|
const data = await apiService.getOptionChainDates(symbol);
|
|
7342
|
-
|
|
7915
|
+
if (this.debug) {
|
|
7916
|
+
console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
|
|
7917
|
+
}
|
|
7343
7918
|
this.availableDates = data.dates_dictionary || {};
|
|
7344
7919
|
this.populateDateOptions();
|
|
7345
7920
|
|
|
@@ -7433,11 +8008,15 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7433
8008
|
this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
|
|
7434
8009
|
}, 10000); // 10 second timeout
|
|
7435
8010
|
|
|
7436
|
-
// Unsubscribe from previous
|
|
8011
|
+
// Unsubscribe from previous subscriptions if they exist
|
|
7437
8012
|
if (this.unsubscribe) {
|
|
7438
8013
|
this.unsubscribe();
|
|
7439
8014
|
this.unsubscribe = null;
|
|
7440
8015
|
}
|
|
8016
|
+
if (this.unsubscribeUnderlying) {
|
|
8017
|
+
this.unsubscribeUnderlying();
|
|
8018
|
+
this.unsubscribeUnderlying = null;
|
|
8019
|
+
}
|
|
7441
8020
|
|
|
7442
8021
|
// Subscribe to option chain data
|
|
7443
8022
|
this.subscribeToData();
|
|
@@ -7447,25 +8026,62 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7447
8026
|
}
|
|
7448
8027
|
}
|
|
7449
8028
|
subscribeToData() {
|
|
7450
|
-
// Subscribe
|
|
7451
|
-
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
|
|
7452
|
-
// Pass symbol as string
|
|
7453
|
-
{
|
|
8029
|
+
// Subscribe to option chain data
|
|
8030
|
+
this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
|
|
7454
8031
|
date: this.date
|
|
7455
|
-
}
|
|
7456
|
-
);
|
|
8032
|
+
});
|
|
7457
8033
|
|
|
7458
|
-
//
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
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);
|
|
7464
8080
|
}
|
|
7465
8081
|
handleData(message) {
|
|
7466
|
-
// Clear loading timeout since we received data
|
|
8082
|
+
// Clear loading timeout since we received data (use BaseWidget method)
|
|
7467
8083
|
if (this.loadingTimeout) {
|
|
7468
|
-
clearTimeout(this.loadingTimeout);
|
|
8084
|
+
this.clearTimeout(this.loadingTimeout);
|
|
7469
8085
|
this.loadingTimeout = null;
|
|
7470
8086
|
}
|
|
7471
8087
|
|
|
@@ -7497,7 +8113,9 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7497
8113
|
}
|
|
7498
8114
|
}
|
|
7499
8115
|
} else {
|
|
7500
|
-
|
|
8116
|
+
// Cache the option chain data
|
|
8117
|
+
this.data = message;
|
|
8118
|
+
this.cachedOptionChainData = message;
|
|
7501
8119
|
this.displayOptionChain(message);
|
|
7502
8120
|
}
|
|
7503
8121
|
} else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
|
|
@@ -7512,9 +8130,35 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7512
8130
|
}
|
|
7513
8131
|
}
|
|
7514
8132
|
} else {
|
|
7515
|
-
|
|
8133
|
+
// Cache the option chain data
|
|
8134
|
+
this.data = message.data;
|
|
8135
|
+
this.cachedOptionChainData = message.data;
|
|
7516
8136
|
this.displayOptionChain(message.data);
|
|
7517
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
|
+
}
|
|
7518
8162
|
}
|
|
7519
8163
|
if (message._cached) {
|
|
7520
8164
|
this.showConnectionQuality(); // Show cache indicator
|
|
@@ -7522,6 +8166,14 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7522
8166
|
}
|
|
7523
8167
|
displayOptionChain(data) {
|
|
7524
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
|
+
}
|
|
7525
8177
|
try {
|
|
7526
8178
|
// Hide loading overlay
|
|
7527
8179
|
this.hideLoading();
|
|
@@ -7554,9 +8206,18 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7554
8206
|
}
|
|
7555
8207
|
});
|
|
7556
8208
|
|
|
7557
|
-
// Sort strikes
|
|
8209
|
+
// Sort strikes
|
|
7558
8210
|
const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
7559
|
-
|
|
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 => {
|
|
7560
8221
|
const {
|
|
7561
8222
|
call,
|
|
7562
8223
|
put
|
|
@@ -7575,7 +8236,7 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7575
8236
|
const formatted = parseFloat(change).toFixed(2);
|
|
7576
8237
|
const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
|
|
7577
8238
|
const sign = change > 0 ? '+' : '';
|
|
7578
|
-
span.className =
|
|
8239
|
+
span.className = className;
|
|
7579
8240
|
span.textContent = `${sign}${formatted}`;
|
|
7580
8241
|
return span;
|
|
7581
8242
|
};
|
|
@@ -7588,7 +8249,28 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7588
8249
|
// Create calls data section
|
|
7589
8250
|
const callsData = document.createElement('div');
|
|
7590
8251
|
callsData.className = 'calls-data';
|
|
7591
|
-
|
|
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);
|
|
7592
8274
|
callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
|
|
7593
8275
|
const callChangeSpan = document.createElement('span');
|
|
7594
8276
|
if (call) {
|
|
@@ -7611,7 +8293,28 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7611
8293
|
// Create puts data section
|
|
7612
8294
|
const putsData = document.createElement('div');
|
|
7613
8295
|
putsData.className = 'puts-data';
|
|
7614
|
-
|
|
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);
|
|
7615
8318
|
putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
|
|
7616
8319
|
const putChangeSpan = document.createElement('span');
|
|
7617
8320
|
if (put) {
|
|
@@ -7635,14 +8338,22 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7635
8338
|
|
|
7636
8339
|
// Add footer - with array length check
|
|
7637
8340
|
if (data && data.length > 0) {
|
|
7638
|
-
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);
|
|
7639
8350
|
const lastUpdateElement = this.container.querySelector('.last-update');
|
|
7640
8351
|
if (lastUpdateElement) {
|
|
7641
8352
|
lastUpdateElement.textContent = `Last update: ${timestamp}`;
|
|
7642
8353
|
}
|
|
7643
8354
|
|
|
7644
8355
|
// Update footer
|
|
7645
|
-
const source =
|
|
8356
|
+
const source = isDelayed ? '20 mins delayed' : 'Real-time';
|
|
7646
8357
|
const dataSourceElement = this.container.querySelector('.data-source');
|
|
7647
8358
|
if (dataSourceElement) {
|
|
7648
8359
|
dataSourceElement.textContent = `Source: ${source}`;
|
|
@@ -7665,35 +8376,6 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7665
8376
|
loadingOverlay.classList.add('hidden');
|
|
7666
8377
|
}
|
|
7667
8378
|
}
|
|
7668
|
-
|
|
7669
|
-
/* showInputError(message) {
|
|
7670
|
-
const errorDiv = document.createElement('div');
|
|
7671
|
-
errorDiv.className = 'symbol-error-message';
|
|
7672
|
-
errorDiv.textContent = message;
|
|
7673
|
-
errorDiv.style.cssText = `
|
|
7674
|
-
color: #dc2626;
|
|
7675
|
-
font-size: 12px;
|
|
7676
|
-
margin-bottom: 2px;
|
|
7677
|
-
position: absolute;
|
|
7678
|
-
background: white;
|
|
7679
|
-
z-index: 1000;
|
|
7680
|
-
transform: translateY(-100%);
|
|
7681
|
-
top: -4px;
|
|
7682
|
-
left: 0; // Align with input box
|
|
7683
|
-
`;
|
|
7684
|
-
|
|
7685
|
-
// Ensure the parent is positioned
|
|
7686
|
-
const parent = this.symbolInput;
|
|
7687
|
-
if (parent) {
|
|
7688
|
-
parent.style.position = 'relative';
|
|
7689
|
-
// Insert the errorDiv as the first child of the parent
|
|
7690
|
-
parent.insertBefore(errorDiv, parent.firstChild);
|
|
7691
|
-
}
|
|
7692
|
-
|
|
7693
|
-
this.symbolInput.errorMessage = errorDiv;
|
|
7694
|
-
this.symbolInput.focus();
|
|
7695
|
-
} */
|
|
7696
|
-
|
|
7697
8379
|
showError(message) {
|
|
7698
8380
|
this.hideLoading();
|
|
7699
8381
|
this.clearError();
|
|
@@ -7766,19 +8448,135 @@ class OptionChainWidget extends BaseWidget {
|
|
|
7766
8448
|
this.container.appendChild(noDataElement);
|
|
7767
8449
|
}
|
|
7768
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
|
+
}
|
|
7769
8546
|
destroy() {
|
|
7770
8547
|
this.isDestroyed = true;
|
|
8548
|
+
|
|
8549
|
+
// Close modal if open
|
|
8550
|
+
this.closeOptionsModal();
|
|
7771
8551
|
if (this.unsubscribe) {
|
|
7772
8552
|
this.unsubscribe();
|
|
7773
8553
|
this.unsubscribe = null;
|
|
7774
8554
|
}
|
|
7775
8555
|
|
|
8556
|
+
// Unsubscribe from underlying price
|
|
8557
|
+
if (this.unsubscribeUnderlying) {
|
|
8558
|
+
this.unsubscribeUnderlying();
|
|
8559
|
+
this.unsubscribeUnderlying = null;
|
|
8560
|
+
}
|
|
8561
|
+
|
|
7776
8562
|
// MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
|
|
7777
8563
|
if (this.loadingTimeout) {
|
|
7778
8564
|
this.clearTimeout(this.loadingTimeout);
|
|
7779
8565
|
this.loadingTimeout = null;
|
|
7780
8566
|
}
|
|
7781
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
|
+
|
|
7782
8580
|
// Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
|
|
7783
8581
|
super.destroy();
|
|
7784
8582
|
}
|
|
@@ -7864,80 +8662,81 @@ const DataStyles = `
|
|
|
7864
8662
|
font-family: Arial, sans-serif;
|
|
7865
8663
|
}
|
|
7866
8664
|
|
|
7867
|
-
.widget-header {
|
|
8665
|
+
.data-widget .widget-header {
|
|
7868
8666
|
display: flex;
|
|
7869
8667
|
flex-direction: column;
|
|
7870
8668
|
margin-bottom: 10px;
|
|
7871
8669
|
}
|
|
7872
8670
|
|
|
7873
|
-
.symbol {
|
|
8671
|
+
.data-widget .symbol {
|
|
7874
8672
|
font-size: 24px;
|
|
7875
8673
|
font-weight: bold;
|
|
7876
8674
|
margin-right: 10px;
|
|
7877
8675
|
}
|
|
7878
8676
|
|
|
7879
|
-
.company-info {
|
|
8677
|
+
.data-widget .company-info {
|
|
7880
8678
|
font-size: 12px;
|
|
7881
8679
|
color: #666;
|
|
7882
8680
|
}
|
|
7883
8681
|
|
|
7884
|
-
.trading-info {
|
|
8682
|
+
.data-widget .trading-info {
|
|
7885
8683
|
font-size: 12px;
|
|
7886
8684
|
color: #999;
|
|
7887
8685
|
}
|
|
7888
8686
|
|
|
7889
|
-
.price-section {
|
|
8687
|
+
.data-widget .price-section {
|
|
7890
8688
|
display: flex;
|
|
7891
8689
|
align-items: baseline;
|
|
7892
8690
|
margin-bottom: 10px;
|
|
7893
8691
|
}
|
|
7894
8692
|
|
|
7895
|
-
.current-price {
|
|
8693
|
+
.data-widget .current-price {
|
|
7896
8694
|
font-size: 36px;
|
|
7897
8695
|
font-weight: bold;
|
|
7898
8696
|
margin-right: 10px;
|
|
7899
8697
|
}
|
|
7900
8698
|
|
|
7901
|
-
.price-change {
|
|
8699
|
+
.data-widget .price-change {
|
|
7902
8700
|
font-size: 18px;
|
|
7903
8701
|
}
|
|
7904
8702
|
|
|
7905
|
-
.price-change.positive {
|
|
8703
|
+
.data-widget .price-change.positive {
|
|
7906
8704
|
color: green;
|
|
7907
8705
|
}
|
|
7908
8706
|
|
|
7909
|
-
.price-change.negative {
|
|
8707
|
+
.data-widget .price-change.negative {
|
|
7910
8708
|
color: red;
|
|
7911
8709
|
}
|
|
7912
8710
|
|
|
7913
|
-
.bid-ask-section {
|
|
8711
|
+
.data-widget .bid-ask-section {
|
|
7914
8712
|
display: flex;
|
|
7915
8713
|
justify-content: space-between;
|
|
7916
8714
|
margin-bottom: 10px;
|
|
7917
8715
|
}
|
|
7918
8716
|
|
|
7919
|
-
.ask,
|
|
8717
|
+
.data-widget .ask,
|
|
8718
|
+
.data-widget .bid {
|
|
7920
8719
|
display: flex;
|
|
7921
8720
|
align-items: center;
|
|
7922
8721
|
}
|
|
7923
8722
|
|
|
7924
|
-
.label {
|
|
8723
|
+
.data-widget .label {
|
|
7925
8724
|
font-size: 14px;
|
|
7926
8725
|
margin-right: 5px;
|
|
7927
8726
|
}
|
|
7928
8727
|
|
|
7929
|
-
.value {
|
|
8728
|
+
.data-widget .value {
|
|
7930
8729
|
font-size: 16px;
|
|
7931
8730
|
font-weight: bold;
|
|
7932
8731
|
margin-right: 5px;
|
|
7933
8732
|
}
|
|
7934
8733
|
|
|
7935
|
-
.size {
|
|
8734
|
+
.data-widget .size {
|
|
7936
8735
|
font-size: 14px;
|
|
7937
8736
|
color: red;
|
|
7938
8737
|
}
|
|
7939
8738
|
|
|
7940
|
-
.widget-footer {
|
|
8739
|
+
.data-widget .widget-footer {
|
|
7941
8740
|
font-size: 12px;
|
|
7942
8741
|
color: #333;
|
|
7943
8742
|
}
|
|
@@ -8348,76 +9147,50 @@ class CombinedMarketWidget extends BaseWidget {
|
|
|
8348
9147
|
|
|
8349
9148
|
// Subscribe to regular market data (Level 1)
|
|
8350
9149
|
// Create a wrapper to add data type context
|
|
8351
|
-
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'],
|
|
8352
|
-
const {
|
|
8353
|
-
event,
|
|
8354
|
-
data
|
|
8355
|
-
} = messageWrapper;
|
|
8356
|
-
|
|
8357
|
-
// Handle connection events
|
|
8358
|
-
if (event === 'connection') {
|
|
8359
|
-
this.handleConnectionStatus(data);
|
|
8360
|
-
return;
|
|
8361
|
-
}
|
|
8362
|
-
|
|
8363
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8364
|
-
if (event === 'data') {
|
|
8365
|
-
data._dataType = 'market';
|
|
8366
|
-
this.handleMessage({
|
|
8367
|
-
event,
|
|
8368
|
-
data
|
|
8369
|
-
});
|
|
8370
|
-
}
|
|
8371
|
-
}, this.symbol);
|
|
9150
|
+
this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
|
|
8372
9151
|
|
|
8373
9152
|
// Subscribe to night session data (BlueOcean or Bruce)
|
|
8374
|
-
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'],
|
|
8375
|
-
const {
|
|
8376
|
-
event,
|
|
8377
|
-
data
|
|
8378
|
-
} = messageWrapper;
|
|
8379
|
-
|
|
8380
|
-
// Connection already handled by market subscription
|
|
8381
|
-
if (event === 'connection') {
|
|
8382
|
-
return;
|
|
8383
|
-
}
|
|
8384
|
-
|
|
8385
|
-
// For data events, add type context and use base handleMessage pattern
|
|
8386
|
-
if (event === 'data') {
|
|
8387
|
-
data._dataType = 'night';
|
|
8388
|
-
this.handleMessage({
|
|
8389
|
-
event,
|
|
8390
|
-
data
|
|
8391
|
-
});
|
|
8392
|
-
}
|
|
8393
|
-
}, this.symbol);
|
|
9153
|
+
this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
|
|
8394
9154
|
}
|
|
8395
9155
|
handleData(message) {
|
|
8396
9156
|
// Extract data type from metadata
|
|
8397
|
-
const dataType = message.
|
|
9157
|
+
const dataType = message.type;
|
|
8398
9158
|
if (this.debug) {
|
|
8399
9159
|
console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
|
|
8400
9160
|
}
|
|
8401
9161
|
|
|
8402
|
-
//
|
|
8403
|
-
if (
|
|
8404
|
-
|
|
8405
|
-
|
|
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
|
+
}
|
|
8406
9180
|
}
|
|
8407
|
-
return;
|
|
8408
9181
|
}
|
|
8409
9182
|
|
|
8410
9183
|
// Handle error messages from server
|
|
8411
|
-
if (message.type === 'error'
|
|
9184
|
+
if (message.type === 'error' || message.error == true) {
|
|
8412
9185
|
if (this.debug) {
|
|
8413
9186
|
console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
|
|
8414
9187
|
}
|
|
8415
9188
|
// Keep existing cached data visible, just hide loading
|
|
8416
|
-
if (dataType === '
|
|
9189
|
+
if (dataType === 'queryl1' && !this.marketData) {
|
|
8417
9190
|
if (this.debug) {
|
|
8418
9191
|
console.log('[CombinedMarketWidget] No market data available');
|
|
8419
9192
|
}
|
|
8420
|
-
} else if (dataType === '
|
|
9193
|
+
} else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
|
|
8421
9194
|
if (this.debug) {
|
|
8422
9195
|
console.log('[CombinedMarketWidget] No night session data available');
|
|
8423
9196
|
}
|
|
@@ -8870,7 +9643,7 @@ const IntradayChartStyles = `
|
|
|
8870
9643
|
margin: 0 auto;
|
|
8871
9644
|
}
|
|
8872
9645
|
|
|
8873
|
-
.chart-header {
|
|
9646
|
+
.intraday-chart-widget .chart-header {
|
|
8874
9647
|
display: flex;
|
|
8875
9648
|
justify-content: space-between;
|
|
8876
9649
|
align-items: center;
|
|
@@ -8879,13 +9652,13 @@ const IntradayChartStyles = `
|
|
|
8879
9652
|
border-bottom: 1px solid #e5e7eb;
|
|
8880
9653
|
}
|
|
8881
9654
|
|
|
8882
|
-
.chart-title-section {
|
|
9655
|
+
.intraday-chart-widget .chart-title-section {
|
|
8883
9656
|
display: flex;
|
|
8884
9657
|
flex-direction: column;
|
|
8885
9658
|
gap: 4px;
|
|
8886
9659
|
}
|
|
8887
9660
|
|
|
8888
|
-
.company-market-info {
|
|
9661
|
+
.intraday-chart-widget .company-market-info {
|
|
8889
9662
|
display: flex;
|
|
8890
9663
|
gap: 8px;
|
|
8891
9664
|
align-items: center;
|
|
@@ -8893,18 +9666,18 @@ const IntradayChartStyles = `
|
|
|
8893
9666
|
color: #6b7280;
|
|
8894
9667
|
}
|
|
8895
9668
|
|
|
8896
|
-
.intraday-company-name {
|
|
9669
|
+
.intraday-chart-widget .intraday-company-name {
|
|
8897
9670
|
font-weight: 500;
|
|
8898
9671
|
}
|
|
8899
9672
|
|
|
8900
|
-
.intraday-chart-symbol {
|
|
9673
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
8901
9674
|
font-size: 1.5em;
|
|
8902
9675
|
font-weight: 700;
|
|
8903
9676
|
color: #1f2937;
|
|
8904
9677
|
margin: 0;
|
|
8905
9678
|
}
|
|
8906
9679
|
|
|
8907
|
-
.intraday-chart-source {
|
|
9680
|
+
.intraday-chart-widget .intraday-chart-source {
|
|
8908
9681
|
font-size: 0.75em;
|
|
8909
9682
|
padding: 4px 8px;
|
|
8910
9683
|
border-radius: 4px;
|
|
@@ -8913,36 +9686,36 @@ const IntradayChartStyles = `
|
|
|
8913
9686
|
font-weight: 600;
|
|
8914
9687
|
}
|
|
8915
9688
|
|
|
8916
|
-
.chart-change {
|
|
9689
|
+
.intraday-chart-widget .chart-change {
|
|
8917
9690
|
font-size: 1.1em;
|
|
8918
9691
|
font-weight: 600;
|
|
8919
9692
|
padding: 6px 12px;
|
|
8920
9693
|
border-radius: 6px;
|
|
8921
9694
|
}
|
|
8922
9695
|
|
|
8923
|
-
.chart-change.positive {
|
|
9696
|
+
.intraday-chart-widget .chart-change.positive {
|
|
8924
9697
|
color: #059669;
|
|
8925
9698
|
background: #d1fae5;
|
|
8926
9699
|
}
|
|
8927
9700
|
|
|
8928
|
-
.chart-change.negative {
|
|
9701
|
+
.intraday-chart-widget .chart-change.negative {
|
|
8929
9702
|
color: #dc2626;
|
|
8930
9703
|
background: #fee2e2;
|
|
8931
9704
|
}
|
|
8932
9705
|
|
|
8933
|
-
.chart-controls {
|
|
9706
|
+
.intraday-chart-widget .chart-controls {
|
|
8934
9707
|
display: flex;
|
|
8935
9708
|
justify-content: space-between;
|
|
8936
9709
|
align-items: center;
|
|
8937
9710
|
margin-bottom: 15px;
|
|
8938
9711
|
}
|
|
8939
9712
|
|
|
8940
|
-
.chart-range-selector {
|
|
9713
|
+
.intraday-chart-widget .chart-range-selector {
|
|
8941
9714
|
display: flex;
|
|
8942
9715
|
gap: 8px;
|
|
8943
9716
|
}
|
|
8944
9717
|
|
|
8945
|
-
.range-btn {
|
|
9718
|
+
.intraday-chart-widget .range-btn {
|
|
8946
9719
|
padding: 8px 16px;
|
|
8947
9720
|
border: 1px solid #e5e7eb;
|
|
8948
9721
|
background: white;
|
|
@@ -8954,28 +9727,28 @@ const IntradayChartStyles = `
|
|
|
8954
9727
|
transition: all 0.2s ease;
|
|
8955
9728
|
}
|
|
8956
9729
|
|
|
8957
|
-
.range-btn:hover {
|
|
9730
|
+
.intraday-chart-widget .range-btn:hover {
|
|
8958
9731
|
background: #f9fafb;
|
|
8959
9732
|
border-color: #d1d5db;
|
|
8960
9733
|
}
|
|
8961
9734
|
|
|
8962
|
-
.range-btn.active {
|
|
9735
|
+
.intraday-chart-widget .range-btn.active {
|
|
8963
9736
|
background: #667eea;
|
|
8964
9737
|
color: white;
|
|
8965
9738
|
border-color: #667eea;
|
|
8966
9739
|
}
|
|
8967
9740
|
|
|
8968
|
-
.range-btn:focus {
|
|
9741
|
+
.intraday-chart-widget .range-btn:focus {
|
|
8969
9742
|
outline: none;
|
|
8970
9743
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
8971
9744
|
}
|
|
8972
9745
|
|
|
8973
|
-
.chart-type-selector {
|
|
9746
|
+
.intraday-chart-widget .chart-type-selector {
|
|
8974
9747
|
display: flex;
|
|
8975
9748
|
gap: 8px;
|
|
8976
9749
|
}
|
|
8977
9750
|
|
|
8978
|
-
.type-btn {
|
|
9751
|
+
.intraday-chart-widget .type-btn {
|
|
8979
9752
|
padding: 8px 16px;
|
|
8980
9753
|
border: 1px solid #e5e7eb;
|
|
8981
9754
|
background: white;
|
|
@@ -8987,23 +9760,23 @@ const IntradayChartStyles = `
|
|
|
8987
9760
|
transition: all 0.2s ease;
|
|
8988
9761
|
}
|
|
8989
9762
|
|
|
8990
|
-
.type-btn:hover {
|
|
9763
|
+
.intraday-chart-widget .type-btn:hover {
|
|
8991
9764
|
background: #f9fafb;
|
|
8992
9765
|
border-color: #d1d5db;
|
|
8993
9766
|
}
|
|
8994
9767
|
|
|
8995
|
-
.type-btn.active {
|
|
9768
|
+
.intraday-chart-widget .type-btn.active {
|
|
8996
9769
|
background: #10b981;
|
|
8997
9770
|
color: white;
|
|
8998
9771
|
border-color: #10b981;
|
|
8999
9772
|
}
|
|
9000
9773
|
|
|
9001
|
-
.type-btn:focus {
|
|
9774
|
+
.intraday-chart-widget .type-btn:focus {
|
|
9002
9775
|
outline: none;
|
|
9003
9776
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
|
9004
9777
|
}
|
|
9005
9778
|
|
|
9006
|
-
.zoom-reset-btn {
|
|
9779
|
+
.intraday-chart-widget .zoom-reset-btn {
|
|
9007
9780
|
display: flex;
|
|
9008
9781
|
align-items: center;
|
|
9009
9782
|
gap: 6px;
|
|
@@ -9018,34 +9791,34 @@ const IntradayChartStyles = `
|
|
|
9018
9791
|
transition: all 0.2s ease;
|
|
9019
9792
|
}
|
|
9020
9793
|
|
|
9021
|
-
.zoom-reset-btn:hover {
|
|
9794
|
+
.intraday-chart-widget .zoom-reset-btn:hover {
|
|
9022
9795
|
background: #f9fafb;
|
|
9023
9796
|
border-color: #667eea;
|
|
9024
9797
|
color: #667eea;
|
|
9025
9798
|
}
|
|
9026
9799
|
|
|
9027
|
-
.zoom-reset-btn:focus {
|
|
9800
|
+
.intraday-chart-widget .zoom-reset-btn:focus {
|
|
9028
9801
|
outline: none;
|
|
9029
9802
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
9030
9803
|
}
|
|
9031
9804
|
|
|
9032
|
-
.zoom-reset-btn svg {
|
|
9805
|
+
.intraday-chart-widget .zoom-reset-btn svg {
|
|
9033
9806
|
flex-shrink: 0;
|
|
9034
9807
|
}
|
|
9035
9808
|
|
|
9036
|
-
.chart-container {
|
|
9809
|
+
.intraday-chart-widget .chart-container {
|
|
9037
9810
|
height: 500px;
|
|
9038
9811
|
margin-bottom: 20px;
|
|
9039
9812
|
position: relative;
|
|
9040
9813
|
}
|
|
9041
9814
|
|
|
9042
|
-
.chart-stats {
|
|
9815
|
+
.intraday-chart-widget .chart-stats {
|
|
9043
9816
|
padding: 15px;
|
|
9044
9817
|
background: #f9fafb;
|
|
9045
9818
|
border-radius: 8px;
|
|
9046
9819
|
}
|
|
9047
9820
|
|
|
9048
|
-
.stats-header {
|
|
9821
|
+
.intraday-chart-widget .stats-header {
|
|
9049
9822
|
font-size: 0.875em;
|
|
9050
9823
|
font-weight: 700;
|
|
9051
9824
|
color: #374151;
|
|
@@ -9056,19 +9829,19 @@ const IntradayChartStyles = `
|
|
|
9056
9829
|
border-bottom: 2px solid #e5e7eb;
|
|
9057
9830
|
}
|
|
9058
9831
|
|
|
9059
|
-
.stats-grid {
|
|
9832
|
+
.intraday-chart-widget .stats-grid {
|
|
9060
9833
|
display: grid;
|
|
9061
9834
|
grid-template-columns: repeat(5, 1fr);
|
|
9062
9835
|
gap: 15px;
|
|
9063
9836
|
}
|
|
9064
9837
|
|
|
9065
|
-
.stat-item {
|
|
9838
|
+
.intraday-chart-widget .stat-item {
|
|
9066
9839
|
display: flex;
|
|
9067
9840
|
flex-direction: column;
|
|
9068
9841
|
gap: 4px;
|
|
9069
9842
|
}
|
|
9070
9843
|
|
|
9071
|
-
.stat-label {
|
|
9844
|
+
.intraday-chart-widget .stat-label {
|
|
9072
9845
|
font-size: 0.75em;
|
|
9073
9846
|
color: #6b7280;
|
|
9074
9847
|
font-weight: 600;
|
|
@@ -9076,13 +9849,13 @@ const IntradayChartStyles = `
|
|
|
9076
9849
|
letter-spacing: 0.5px;
|
|
9077
9850
|
}
|
|
9078
9851
|
|
|
9079
|
-
.stat-value {
|
|
9852
|
+
.intraday-chart-widget .stat-value {
|
|
9080
9853
|
font-size: 1.1em;
|
|
9081
9854
|
font-weight: 700;
|
|
9082
9855
|
color: #1f2937;
|
|
9083
9856
|
}
|
|
9084
9857
|
|
|
9085
|
-
.widget-loading-overlay {
|
|
9858
|
+
.intraday-chart-widget .widget-loading-overlay {
|
|
9086
9859
|
position: absolute;
|
|
9087
9860
|
top: 0;
|
|
9088
9861
|
left: 0;
|
|
@@ -9101,26 +9874,26 @@ const IntradayChartStyles = `
|
|
|
9101
9874
|
backdrop-filter: blur(1px);
|
|
9102
9875
|
}
|
|
9103
9876
|
|
|
9104
|
-
.widget-loading-overlay.hidden {
|
|
9877
|
+
.intraday-chart-widget .widget-loading-overlay.hidden {
|
|
9105
9878
|
display: none;
|
|
9106
9879
|
}
|
|
9107
9880
|
|
|
9108
|
-
.loading-spinner {
|
|
9881
|
+
.intraday-chart-widget .loading-spinner {
|
|
9109
9882
|
width: 20px;
|
|
9110
9883
|
height: 20px;
|
|
9111
9884
|
border: 3px solid #e5e7eb;
|
|
9112
9885
|
border-top-color: #667eea;
|
|
9113
9886
|
border-radius: 50%;
|
|
9114
|
-
animation: spin 0.8s linear infinite;
|
|
9887
|
+
animation: intraday-spin 0.8s linear infinite;
|
|
9115
9888
|
}
|
|
9116
9889
|
|
|
9117
|
-
.loading-text {
|
|
9890
|
+
.intraday-chart-widget .loading-text {
|
|
9118
9891
|
color: #6b7280;
|
|
9119
9892
|
font-size: 0.875em;
|
|
9120
9893
|
font-weight: 500;
|
|
9121
9894
|
}
|
|
9122
9895
|
|
|
9123
|
-
@keyframes spin {
|
|
9896
|
+
@keyframes intraday-spin {
|
|
9124
9897
|
to { transform: rotate(360deg); }
|
|
9125
9898
|
}
|
|
9126
9899
|
|
|
@@ -9130,26 +9903,26 @@ const IntradayChartStyles = `
|
|
|
9130
9903
|
padding: 15px;
|
|
9131
9904
|
}
|
|
9132
9905
|
|
|
9133
|
-
.stats-grid {
|
|
9906
|
+
.intraday-chart-widget .stats-grid {
|
|
9134
9907
|
grid-template-columns: repeat(3, 1fr);
|
|
9135
9908
|
gap: 10px;
|
|
9136
9909
|
}
|
|
9137
9910
|
|
|
9138
|
-
.stats-header {
|
|
9911
|
+
.intraday-chart-widget .stats-header {
|
|
9139
9912
|
font-size: 0.8em;
|
|
9140
9913
|
margin-bottom: 10px;
|
|
9141
9914
|
}
|
|
9142
9915
|
|
|
9143
|
-
.chart-container {
|
|
9916
|
+
.intraday-chart-widget .chart-container {
|
|
9144
9917
|
height: 350px;
|
|
9145
9918
|
}
|
|
9146
9919
|
|
|
9147
|
-
.intraday-chart-symbol {
|
|
9920
|
+
.intraday-chart-widget .intraday-chart-symbol {
|
|
9148
9921
|
font-size: 1.2em;
|
|
9149
9922
|
}
|
|
9150
9923
|
}
|
|
9151
9924
|
|
|
9152
|
-
.widget-error {
|
|
9925
|
+
.intraday-chart-widget .widget-error {
|
|
9153
9926
|
padding: 15px;
|
|
9154
9927
|
background: #fee2e2;
|
|
9155
9928
|
border: 1px solid #fecaca;
|
|
@@ -42019,8 +42792,8 @@ class WebSocketManager {
|
|
|
42019
42792
|
// Pass messageType from parent message, don't extract from each data item
|
|
42020
42793
|
this._processDataItem(dataItem, relevantWidgets, messageType);
|
|
42021
42794
|
});
|
|
42022
|
-
} else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
|
|
42023
|
-
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);
|
|
42024
42797
|
} else {
|
|
42025
42798
|
// Process single item - messageType already extracted from message
|
|
42026
42799
|
this._processDataItem(item, relevantWidgets, messageType);
|
|
@@ -42029,6 +42802,7 @@ class WebSocketManager {
|
|
|
42029
42802
|
|
|
42030
42803
|
// Simplified option chain processing - handle entire array at once
|
|
42031
42804
|
_processOptionChainData(optionChainArray, relevantWidgets) {
|
|
42805
|
+
//console.log('PROCESSING DATA OPTIONS')
|
|
42032
42806
|
if (!optionChainArray || optionChainArray.length === 0) return;
|
|
42033
42807
|
// Extract underlying symbol and date from first option contract
|
|
42034
42808
|
const firstOption = optionChainArray[0];
|