Add candlestick pattern indicators with TA-Lib integration

- Add 15 candlestick pattern indicators (Doji, Hammer, Engulfing, etc.)
- Create dedicated Patterns chart pane for pattern visualization
- Add hover tooltips with descriptions and SVG diagrams in indicator selector
- Fix color picker compatibility by using hex format instead of RGBA
- Fix chart positioning and timestamp synchronization for pattern chart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-08 05:45:51 -03:00
parent 7b796c51c8
commit 4ab7a5023d
8 changed files with 664 additions and 20 deletions

View File

@ -302,6 +302,160 @@ class MACD(Indicator):
return df.iloc[self.properties['signal_p']:] return df.iloc[self.properties['signal_p']:]
# ============================================================================
# Candlestick Pattern Recognition Indicators
# ============================================================================
class CandlestickPattern(Indicator):
"""
Base class for candlestick pattern recognition indicators.
Pattern indicators return discrete signals:
- 100 = Bullish pattern detected
- -100 = Bearish pattern detected
- 0 = No pattern
All patterns require OHLC data (open, high, low, close).
"""
def __init__(self, name: str, indicator_type: str, properties: dict):
super().__init__(name, indicator_type, properties)
# Default display properties for candlestick patterns (hex format for color picker compatibility)
self.properties.setdefault('bullish_color', '#00C853')
self.properties.setdefault('bearish_color', '#FF5252')
# Patterns don't use period in the same way - set to 0 to avoid warmup trimming
self.properties.setdefault('period', 0)
def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
"""
Calculate pattern detection across all candles.
Returns a DataFrame with 'time' and 'value' columns.
"""
# Extract OHLC data as numpy arrays
opens = candles.open.to_numpy(dtype='float')
highs = candles.high.to_numpy(dtype='float')
lows = candles.low.to_numpy(dtype='float')
closes = candles.close.to_numpy(dtype='float')
# Call the pattern-specific detection method
pattern_values = self.detect(opens, highs, lows, closes)
# Store the latest value
self.properties['value'] = int(pattern_values[-1])
# Create DataFrame with signal values
df = pd.DataFrame({
'time': candles.time,
'value': pattern_values.astype(int).tolist()
})
return df
def detect(self, opens: np.ndarray, highs: np.ndarray,
lows: np.ndarray, closes: np.ndarray) -> np.ndarray:
"""
Override in subclass to call specific TA-Lib pattern function.
Must return a numpy array of pattern signals (100, -100, or 0).
"""
raise NotImplementedError("Subclasses must implement the 'detect' method.")
# Single Candle Patterns (6)
class CDL_Doji(CandlestickPattern):
"""Doji - Indecision pattern where open and close are nearly equal."""
def detect(self, opens, highs, lows, closes):
return talib.CDLDOJI(opens, highs, lows, closes)
class CDL_Hammer(CandlestickPattern):
"""Hammer - Bullish reversal pattern with small body and long lower shadow."""
def detect(self, opens, highs, lows, closes):
return talib.CDLHAMMER(opens, highs, lows, closes)
class CDL_InvertedHammer(CandlestickPattern):
"""Inverted Hammer - Bullish reversal pattern with small body and long upper shadow."""
def detect(self, opens, highs, lows, closes):
return talib.CDLINVERTEDHAMMER(opens, highs, lows, closes)
class CDL_ShootingStar(CandlestickPattern):
"""Shooting Star - Bearish reversal pattern with small body and long upper shadow."""
def detect(self, opens, highs, lows, closes):
return talib.CDLSHOOTINGSTAR(opens, highs, lows, closes)
class CDL_SpinningTop(CandlestickPattern):
"""Spinning Top - Indecision pattern with small body and equal shadows."""
def detect(self, opens, highs, lows, closes):
return talib.CDLSPINNINGTOP(opens, highs, lows, closes)
class CDL_Marubozu(CandlestickPattern):
"""Marubozu - Strong trend candle with no shadows (or very small)."""
def detect(self, opens, highs, lows, closes):
return talib.CDLMARUBOZU(opens, highs, lows, closes)
# Two Candle Patterns (4)
class CDL_Engulfing(CandlestickPattern):
"""Engulfing - Reversal pattern where second candle completely engulfs the first."""
def detect(self, opens, highs, lows, closes):
return talib.CDLENGULFING(opens, highs, lows, closes)
class CDL_Harami(CandlestickPattern):
"""Harami - Reversal pattern where second candle is contained within the first."""
def detect(self, opens, highs, lows, closes):
return talib.CDLHARAMI(opens, highs, lows, closes)
class CDL_Piercing(CandlestickPattern):
"""Piercing Line - Bullish reversal where second candle opens below and closes above midpoint."""
def detect(self, opens, highs, lows, closes):
return talib.CDLPIERCING(opens, highs, lows, closes)
class CDL_DarkCloudCover(CandlestickPattern):
"""Dark Cloud Cover - Bearish reversal where second candle opens above and closes below midpoint."""
def detect(self, opens, highs, lows, closes):
return talib.CDLDARKCLOUDCOVER(opens, highs, lows, closes)
# Three+ Candle Patterns (5)
class CDL_MorningStar(CandlestickPattern):
"""Morning Star - Bullish reversal with three candles: bearish, small body, bullish."""
def detect(self, opens, highs, lows, closes):
return talib.CDLMORNINGSTAR(opens, highs, lows, closes)
class CDL_EveningStar(CandlestickPattern):
"""Evening Star - Bearish reversal with three candles: bullish, small body, bearish."""
def detect(self, opens, highs, lows, closes):
return talib.CDLEVENINGSTAR(opens, highs, lows, closes)
class CDL_ThreeWhiteSoldiers(CandlestickPattern):
"""Three White Soldiers - Strong bullish pattern with three consecutive bullish candles."""
def detect(self, opens, highs, lows, closes):
return talib.CDL3WHITESOLDIERS(opens, highs, lows, closes)
class CDL_ThreeBlackCrows(CandlestickPattern):
"""Three Black Crows - Strong bearish pattern with three consecutive bearish candles."""
def detect(self, opens, highs, lows, closes):
return talib.CDL3BLACKCROWS(opens, highs, lows, closes)
class CDL_ThreeInside(CandlestickPattern):
"""Three Inside Up/Down - Reversal pattern confirming a harami with a third candle."""
def detect(self, opens, highs, lows, closes):
return talib.CDL3INSIDE(opens, highs, lows, closes)
# Register indicators in the registry # Register indicators in the registry
indicators_registry['Volume'] = Volume indicators_registry['Volume'] = Volume
indicators_registry['SMA'] = SMA indicators_registry['SMA'] = SMA
@ -313,6 +467,26 @@ indicators_registry['BOLBands'] = BolBands
indicators_registry['BOL%B'] = BollingerPercentB indicators_registry['BOL%B'] = BollingerPercentB
indicators_registry['MACD'] = MACD indicators_registry['MACD'] = MACD
# Register candlestick pattern indicators
# Single Candle Patterns
indicators_registry['CDL_DOJI'] = CDL_Doji
indicators_registry['CDL_HAMMER'] = CDL_Hammer
indicators_registry['CDL_INVERTEDHAMMER'] = CDL_InvertedHammer
indicators_registry['CDL_SHOOTINGSTAR'] = CDL_ShootingStar
indicators_registry['CDL_SPINNINGTOP'] = CDL_SpinningTop
indicators_registry['CDL_MARUBOZU'] = CDL_Marubozu
# Two Candle Patterns
indicators_registry['CDL_ENGULFING'] = CDL_Engulfing
indicators_registry['CDL_HARAMI'] = CDL_Harami
indicators_registry['CDL_PIERCING'] = CDL_Piercing
indicators_registry['CDL_DARKCLOUDCOVER'] = CDL_DarkCloudCover
# Three+ Candle Patterns
indicators_registry['CDL_MORNINGSTAR'] = CDL_MorningStar
indicators_registry['CDL_EVENINGSTAR'] = CDL_EveningStar
indicators_registry['CDL_3WHITESOLDIERS'] = CDL_ThreeWhiteSoldiers
indicators_registry['CDL_3BLACKCROWS'] = CDL_ThreeBlackCrows
indicators_registry['CDL_3INSIDE'] = CDL_ThreeInside
class Indicators: class Indicators:
""" """

View File

@ -367,6 +367,14 @@ height: 500px;
grid-column: 1; grid-column: 1;
grid-row:4; grid-row:4;
} }
#chart4{
grid-column: 1;
grid-row:5;
}
#chart5{
grid-column: 1;
grid-row:6;
}
/***************************************************/ /***************************************************/
/****************Right Panel***********************/ /****************Right Panel***********************/

View File

@ -7,6 +7,7 @@ class Charts {
this.chart2_id = idata.chart2_id; this.chart2_id = idata.chart2_id;
this.chart3_id = idata.chart3_id; this.chart3_id = idata.chart3_id;
this.chart4_id = idata.chart4_id; this.chart4_id = idata.chart4_id;
this.chart5_id = idata.chart5_id; // Patterns chart
this.trading_pair = idata.trading_pair; this.trading_pair = idata.trading_pair;
this.price_history = idata.price_history; this.price_history = idata.price_history;
/* A list of bound charts this is necessary for maintaining a dynamic /* A list of bound charts this is necessary for maintaining a dynamic
@ -68,6 +69,23 @@ class Charts {
this.bind_charts(this.chart4); this.bind_charts(this.chart4);
} }
create_patterns_chart(){
// Create a chart for candlestick pattern indicators
// Scale is fixed -100 to +100 (bearish to bullish signals)
this.chart5 = this.create_chart(this.chart5_id, 100);
this.set_priceScale(this.chart5, 0.1, 0.1);
// Put the name of the chart in a watermark
this.addWatermark(this.chart5, 'Patterns');
this.bind_charts(this.chart5);
// Sync time scale with main chart so timestamps align
if (this.chart_1) {
let barSpacing = this.chart_1.timeScale().getBarSpacing();
let scrollPosition = this.chart_1.timeScale().scrollPosition();
this.chart5.timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
}
}
create_chart(target_id, height=500){ create_chart(target_id, height=500){
// Accepts a target element to place the chart in. // Accepts a target element to place the chart in.
@ -148,6 +166,9 @@ class Charts {
// if bound_charts has four elements in it bind them // if bound_charts has four elements in it bind them
if (bcl == 4) { this.bind4charts(); } if (bcl == 4) { this.bind4charts(); }
// if bound_charts has five elements in it bind them
if (bcl == 5) { this.bind5charts(); }
return; return;
} }
add_to_list(chart){ add_to_list(chart){
@ -233,6 +254,26 @@ class Charts {
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3)); this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
} }
bind5charts(){
// Sync all 5 charts together (main + RSI + MACD + %B + Patterns)
let syncFromChart = (sourceIndex) => {
return (e) => {
let barSpacing = this.bound_charts[sourceIndex].timeScale().getBarSpacing();
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
for (let i = 0; i < 5; i++) {
if (i !== sourceIndex) {
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
}
}
}
}
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
this.bound_charts[4].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(4));
}
// Set trade markers on chart for all trades in backtest results // Set trade markers on chart for all trades in backtest results
setTradeMarkers(trades) { setTradeMarkers(trades) {
if (!this.candleSeries) { if (!this.candleSeries) {

View File

@ -8,6 +8,7 @@ class Data {
this.chart2_id = 'chart2'; this.chart2_id = 'chart2';
this.chart3_id = 'chart3'; this.chart3_id = 'chart3';
this.chart4_id = 'chart4'; this.chart4_id = 'chart4';
this.chart5_id = 'chart5'; // Candlestick patterns chart
/* Set into memory configuration data from the server. */ /* Set into memory configuration data from the server. */
// The assets being traded. // The assets being traded.

View File

@ -49,6 +49,7 @@ class User_Interface {
chart2_id: this.data.chart2_id, chart2_id: this.data.chart2_id,
chart3_id: this.data.chart3_id, chart3_id: this.data.chart3_id,
chart4_id: this.data.chart4_id, chart4_id: this.data.chart4_id,
chart5_id: this.data.chart5_id,
trading_pair: this.data.trading_pair, trading_pair: this.data.trading_pair,
price_history: this.data.price_history price_history: this.data.price_history
}; };

View File

@ -545,6 +545,111 @@ class Bolenger extends Indicator {
} }
indicatorMap.set("BOLBands", Bolenger); indicatorMap.set("BOLBands", Bolenger);
// Candlestick Pattern Indicator class
// Displays pattern signals as histogram bars in a dedicated patterns chart
class CandlestickPattern extends Indicator {
constructor(name, charts, bullish_color, bearish_color) {
super(name);
// Create patterns chart if it doesn't exist
if (!charts.hasOwnProperty('chart5') || !charts.chart5) {
charts.create_patterns_chart();
}
let chart = charts.chart5;
// Store colors for pattern signals (hex format for color picker compatibility)
this.bullish_color = bullish_color || '#00C853';
this.bearish_color = bearish_color || '#FF5252';
// Add histogram series for this pattern
this.addPatternHist(name, chart);
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'charts', 'bullish_color', 'bearish_color'],
class: this
};
}
addPatternHist(name, chart) {
// Create histogram series for pattern signals
this.hist[name] = chart.addHistogramSeries({
priceFormat: {
type: 'price',
},
priceScaleId: 'right',
});
// Create legend for the pattern
iOutput.create_legend(`${this.name}_pattern`, chart, this.hist[name]);
}
init(data) {
// Transform data to add colors based on signal value
// Keep ALL data points (including zeros) to maintain proper time scale alignment
// Zero values will have no visible bar (height = 0) but the timestamp is needed
const histData = data.map(d => ({
time: d.time,
value: d.value,
// Use bullish/bearish colors for signals, gray for no signal (won't be visible anyway)
color: d.value > 0 ? this.bullish_color
: d.value < 0 ? this.bearish_color
: '#808080'
}));
this.setHist(this.name, histData);
// Update display with latest signal
const latestValue = data.at(-1).value;
let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None';
this.updateDisplay(this.name, displayText, 'value');
}
update(data) {
// Transform data to add colors based on signal value
// Keep ALL data points to maintain time scale alignment with other charts
const histData = data.map(d => ({
time: d.time,
value: d.value,
color: d.value > 0 ? this.bullish_color
: d.value < 0 ? this.bearish_color
: '#808080'
}));
this.setHist(this.name, histData);
// Update display with latest signal
const latestValue = data.at(-1).value;
let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None';
this.updateDisplay(this.name, displayText, 'value');
}
updateDisplay(name, value, value_name) {
// Override to handle text display for patterns
let element = document.getElementById(this.name + '_' + value_name);
if (element) {
element.value = value;
}
}
}
// Register all candlestick pattern types to use the same CandlestickPattern class
indicatorMap.set("CDL_DOJI", CandlestickPattern);
indicatorMap.set("CDL_HAMMER", CandlestickPattern);
indicatorMap.set("CDL_INVERTEDHAMMER", CandlestickPattern);
indicatorMap.set("CDL_SHOOTINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_SPINNINGTOP", CandlestickPattern);
indicatorMap.set("CDL_MARUBOZU", CandlestickPattern);
indicatorMap.set("CDL_ENGULFING", CandlestickPattern);
indicatorMap.set("CDL_HARAMI", CandlestickPattern);
indicatorMap.set("CDL_PIERCING", CandlestickPattern);
indicatorMap.set("CDL_DARKCLOUDCOVER", CandlestickPattern);
indicatorMap.set("CDL_MORNINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_EVENINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_3WHITESOLDIERS", CandlestickPattern);
indicatorMap.set("CDL_3BLACKCROWS", CandlestickPattern);
indicatorMap.set("CDL_3INSIDE", CandlestickPattern);
class Indicators { class Indicators {
constructor(comms) { constructor(comms) {
// Contains instantiated indicators. // Contains instantiated indicators.
@ -570,6 +675,8 @@ class Indicators {
if (arg === 'color_1') return indicators[name].color_1 || 'red'; if (arg === 'color_1') return indicators[name].color_1 || 'red';
if (arg === 'color_2') return indicators[name].color_2 || 'white'; if (arg === 'color_2') return indicators[name].color_2 || 'white';
if (arg === 'color_3') return indicators[name].color_3 || 'blue'; if (arg === 'color_3') return indicators[name].color_3 || 'blue';
if (arg === 'bullish_color') return indicators[name].bullish_color || '#00C853';
if (arg === 'bearish_color') return indicators[name].bearish_color || '#FF5252';
}); });
this.i_objs[name] = new IndicatorConstructor(...preparedArgs); this.i_objs[name] = new IndicatorConstructor(...preparedArgs);
@ -658,12 +765,15 @@ class Indicators {
let chart; let chart;
// Determine which chart the indicator is on, based on its type // Determine which chart the indicator is on, based on its type
const indicatorType = this.i_objs[indicator].constructor.name;
if (indicator.includes('RSI')) { if (indicator.includes('RSI')) {
chart = window.UI.charts.chart2; // Assume RSI is on chart2 chart = window.UI.charts.chart2; // Assume RSI is on chart2
} else if (indicator.includes('MACD')) { } else if (indicator.includes('MACD')) {
chart = window.UI.charts.chart3; // Assume MACD is on chart3 chart = window.UI.charts.chart3; // Assume MACD is on chart3
} else if (indicator.includes('%B') || indicator.includes('PercentB')) { } else if (indicator.includes('%B') || indicator.includes('PercentB')) {
chart = window.UI.charts.chart4; // %B is on chart4 chart = window.UI.charts.chart4; // %B is on chart4
} else if (indicatorType === 'CandlestickPattern') {
chart = window.UI.charts.chart5; // Candlestick patterns on chart5
} else { } else {
chart = window.UI.charts.chart_1; // Default to the main chart chart = window.UI.charts.chart_1; // Default to the main chart
} }
@ -906,18 +1016,19 @@ class Indicators {
// Append all selected indicators as a single array-like structure // Append all selected indicators as a single array-like structure
formData.append('indicator', JSON.stringify(selectedIndicators)); formData.append('indicator', JSON.stringify(selectedIndicators));
// Show loading feedback immediately // Hide the popup immediately
const submitBtn = form.querySelector('input[type="submit"]');
const originalValue = submitBtn.value;
submitBtn.value = 'Updating...';
submitBtn.disabled = true;
// Hide the popup and show a brief loading indicator
const popup = document.getElementById('indicators'); const popup = document.getElementById('indicators');
if (popup) { if (popup) {
popup.style.display = 'none'; popup.style.display = 'none';
} }
// Show a loading overlay on the page
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;justify-content:center;align-items:center;z-index:9999;';
overlay.innerHTML = '<div style="background:white;padding:20px 40px;border-radius:10px;font-size:18px;font-weight:bold;">Updating indicators...</div>';
document.body.appendChild(overlay);
// Send form data via AJAX (fetch) // Send form data via AJAX (fetch)
fetch(form.action, { fetch(form.action, {
method: form.method, method: form.method,
@ -927,15 +1038,15 @@ class Indicators {
// Handle success (you can reload the page or update the UI) // Handle success (you can reload the page or update the UI)
window.location.reload(); window.location.reload();
} else { } else {
// Restore button on error // Remove overlay on error
submitBtn.value = originalValue; const overlay = document.getElementById('loading-overlay');
submitBtn.disabled = false; if (overlay) overlay.remove();
alert('Failed to update indicators.'); alert('Failed to update indicators.');
} }
}).catch(error => { }).catch(error => {
console.error('Error:', error); console.error('Error:', error);
submitBtn.value = originalValue; const overlay = document.getElementById('loading-overlay');
submitBtn.disabled = false; if (overlay) overlay.remove();
alert('An error occurred while updating the indicators.'); alert('An error occurred while updating the indicators.');
}); });
} }

View File

@ -49,7 +49,7 @@
<!-- Container for the whole user app --> <!-- Container for the whole user app -->
<div id="master_panel" class="master_panel" <div id="master_panel" class="master_panel"
style="width 1550px; height 800px;display: grid; style="width 1550px; height 800px;display: grid;
grid-template-columns: 1000px 550px; grid-template-rows: 50px 500px 100px 100px;"> grid-template-columns: 1000px 550px; grid-template-rows: 50px 500px 100px 100px 100px 100px;">
<!-- Application Header --> <!-- Application Header -->
<div id="app_header"> <div id="app_header">
<H1 id="app_title">{{ title }}</H1> <H1 id="app_title">{{ title }}</H1>
@ -61,6 +61,7 @@
<div id="chart2"></div> <div id="chart2"></div>
<div id="chart3"></div> <div id="chart3"></div>
<div id="chart4"></div> <div id="chart4"></div>
<div id="chart5"></div> <!-- Candlestick Patterns chart -->
<!-- This is the control panel on the right of the screen --> <!-- This is the control panel on the right of the screen -->
{% include "control_panel.html" %} {% include "control_panel.html" %}
</div><!-- End Master Panel ---> </div><!-- End Master Panel --->

View File

@ -14,14 +14,28 @@
<input type="text" id="newi_name" value="New Indicator" style="width: 100%; margin-top: 5px;"> <input type="text" id="newi_name" value="New Indicator" style="width: 100%; margin-top: 5px;">
</div> </div>
<!-- Type selector --> <!-- Type selector with tooltip -->
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px; position: relative;">
<label for="newi_type"><b>Type:</b></label> <label for="newi_type"><b>Type:</b></label>
<select id="newi_type" style="width: 100%; margin-top: 5px;"> <div class="indicator-type-wrapper" style="position: relative;">
<input type="text" id="newi_type_search" placeholder="Search indicators..."
style="width: 100%; margin-top: 5px; padding: 8px; box-sizing: border-box;"
value="{% if indicator_types %}{{ indicator_types[0] }}{% endif %}"
onfocus="showIndicatorDropdown()" oninput="filterIndicatorTypes()">
<input type="hidden" id="newi_type" value="{% if indicator_types %}{{ indicator_types[0] }}{% endif %}">
<!-- Custom dropdown -->
<div id="indicator_dropdown" class="indicator-dropdown" style="display: none;">
{% for i_type in indicator_types %} {% for i_type in indicator_types %}
<option value="{{i_type}}">{{i_type}}</option> <div class="indicator-option" data-value="{{i_type}}"
onmouseover="showIndicatorTooltip('{{i_type}}')"
onclick="selectIndicatorType('{{i_type}}')">
{{i_type}}
</div>
{% endfor %} {% endfor %}
</select> </div>
</div>
</div> </div>
<!-- Properties display --> <!-- Properties display -->
@ -47,6 +61,8 @@
<option value="color_1"> <option value="color_1">
<option value="color_2"> <option value="color_2">
<option value="color_3"> <option value="color_3">
<option value="bullish_color">
<option value="bearish_color">
</datalist> </datalist>
</div> </div>
<div> <div>
@ -98,3 +114,294 @@
<!-- Resizer --> <!-- Resizer -->
<div id="resize-indicator" class="resize-handle"></div> <div id="resize-indicator" class="resize-handle"></div>
</div> </div>
<!-- Tooltip panel - placed outside dialog to avoid overflow clipping -->
<div id="indicator_tooltip" class="indicator-tooltip">
<div id="tooltip_title" style="font-weight: bold; margin-bottom: 8px;"></div>
<div id="tooltip_description" style="font-size: 12px; margin-bottom: 10px;"></div>
<div id="tooltip_svg" style="text-align: center;"></div>
</div>
<style>
.indicator-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1000;
}
.indicator-option {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.indicator-option:hover {
background-color: #3E3AF2;
color: white;
}
.indicator-option:last-child {
border-bottom: none;
}
.indicator-tooltip {
display: none;
position: fixed;
width: 280px;
padding: 15px;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
z-index: 10001;
}
.indicator-tooltip::before {
display: none;
}
</style>
<script>
// Indicator descriptions and SVGs
const indicatorInfo = {
// Regular indicators
'SMA': {
description: 'Simple Moving Average - Calculates the average price over a specified period. Useful for identifying trends and support/resistance levels.',
svg: null
},
'EMA': {
description: 'Exponential Moving Average - Similar to SMA but gives more weight to recent prices, making it more responsive to new information.',
svg: null
},
'RSI': {
description: 'Relative Strength Index - Momentum oscillator measuring speed and magnitude of price changes. Values above 70 indicate overbought, below 30 oversold.',
svg: null
},
'MACD': {
description: 'Moving Average Convergence Divergence - Shows relationship between two EMAs. Used to identify trend direction, momentum, and potential reversals.',
svg: null
},
'BOLBands': {
description: 'Bollinger Bands - Volatility indicator with upper and lower bands around a moving average. Price touching bands may indicate overbought/oversold conditions.',
svg: null
},
'BOL%B': {
description: 'Bollinger %B - Shows where price is relative to the Bollinger Bands. Values above 1 = above upper band, below 0 = below lower band.',
svg: null
},
'ATR': {
description: 'Average True Range - Measures market volatility by calculating the average range between high and low prices over a period.',
svg: null
},
'LREG': {
description: 'Linear Regression - Projects future prices based on a linear trend line fitted to recent price data.',
svg: null
},
'Volume': {
description: 'Trading Volume - Shows the number of units traded. High volume confirms price moves, low volume suggests weak conviction.',
svg: null
},
// Candlestick patterns - Single candle
'CDL_DOJI': {
description: 'Doji - Open and close are nearly equal, forming a cross shape. Indicates market indecision and potential reversal.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<line x1="30" y1="10" x2="30" y2="70" stroke="#333" stroke-width="2"/>
<line x1="20" y1="40" x2="40" y2="40" stroke="#333" stroke-width="3"/>
</svg>`
},
'CDL_HAMMER': {
description: 'Hammer - Small body at top with long lower shadow. Bullish reversal signal appearing after a downtrend.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<line x1="30" y1="15" x2="30" y2="70" stroke="#333" stroke-width="2"/>
<rect x="22" y="15" width="16" height="12" fill="#00C853" stroke="#333"/>
</svg>`
},
'CDL_INVERTEDHAMMER': {
description: 'Inverted Hammer - Small body at bottom with long upper shadow. Potential bullish reversal after downtrend.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<line x1="30" y1="10" x2="30" y2="65" stroke="#333" stroke-width="2"/>
<rect x="22" y="53" width="16" height="12" fill="#00C853" stroke="#333"/>
</svg>`
},
'CDL_SHOOTINGSTAR': {
description: 'Shooting Star - Small body at bottom with long upper shadow. Bearish reversal signal after an uptrend.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<line x1="30" y1="10" x2="30" y2="65" stroke="#333" stroke-width="2"/>
<rect x="22" y="53" width="16" height="12" fill="#FF5252" stroke="#333"/>
</svg>`
},
'CDL_SPINNINGTOP': {
description: 'Spinning Top - Small body with upper and lower shadows. Indicates indecision between buyers and sellers.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<line x1="30" y1="10" x2="30" y2="70" stroke="#333" stroke-width="2"/>
<rect x="22" y="32" width="16" height="16" fill="#888" stroke="#333"/>
</svg>`
},
'CDL_MARUBOZU': {
description: 'Marubozu - Long body with no shadows. Strong bullish (white) or bearish (black) conviction.',
svg: `<svg width="60" height="80" viewBox="0 0 60 80">
<rect x="20" y="10" width="20" height="60" fill="#00C853" stroke="#333" stroke-width="2"/>
</svg>`
},
// Candlestick patterns - Two candle
'CDL_ENGULFING': {
description: 'Engulfing Pattern - Second candle completely engulfs the first. Bullish engulfing after downtrend, bearish after uptrend.',
svg: `<svg width="80" height="80" viewBox="0 0 80 80">
<rect x="12" y="30" width="14" height="25" fill="#FF5252" stroke="#333"/>
<rect x="38" y="20" width="20" height="45" fill="#00C853" stroke="#333" stroke-width="2"/>
</svg>`
},
'CDL_HARAMI': {
description: 'Harami - Second candle is contained within the first. Suggests potential reversal, especially with confirmation.',
svg: `<svg width="80" height="80" viewBox="0 0 80 80">
<rect x="10" y="15" width="20" height="50" fill="#FF5252" stroke="#333" stroke-width="2"/>
<rect x="42" y="30" width="12" height="20" fill="#00C853" stroke="#333"/>
</svg>`
},
'CDL_PIERCING': {
description: 'Piercing Line - Bullish pattern where second candle opens below prior low and closes above midpoint of first candle.',
svg: `<svg width="80" height="80" viewBox="0 0 80 80">
<rect x="12" y="15" width="16" height="40" fill="#FF5252" stroke="#333"/>
<line x1="20" y1="55" x2="20" y2="65" stroke="#333" stroke-width="2"/>
<rect x="42" y="25" width="16" height="40" fill="#00C853" stroke="#333"/>
<line x1="50" y1="65" x2="50" y2="72" stroke="#333" stroke-width="2"/>
</svg>`
},
'CDL_DARKCLOUDCOVER': {
description: 'Dark Cloud Cover - Bearish pattern where second candle opens above prior high and closes below midpoint of first candle.',
svg: `<svg width="80" height="80" viewBox="0 0 80 80">
<rect x="12" y="25" width="16" height="40" fill="#00C853" stroke="#333"/>
<line x1="20" y1="15" x2="20" y2="25" stroke="#333" stroke-width="2"/>
<rect x="42" y="15" width="16" height="40" fill="#FF5252" stroke="#333"/>
<line x1="50" y1="55" x2="50" y2="65" stroke="#333" stroke-width="2"/>
</svg>`
},
// Candlestick patterns - Three+ candle
'CDL_MORNINGSTAR': {
description: 'Morning Star - Three-candle bullish reversal: large bearish, small body (star), large bullish. Strong reversal signal.',
svg: `<svg width="100" height="80" viewBox="0 0 100 80">
<rect x="8" y="15" width="14" height="40" fill="#FF5252" stroke="#333"/>
<rect x="36" y="50" width="10" height="8" fill="#888" stroke="#333"/>
<line x1="41" y1="45" x2="41" y2="50" stroke="#333" stroke-width="2"/>
<line x1="41" y1="58" x2="41" y2="65" stroke="#333" stroke-width="2"/>
<rect x="60" y="20" width="14" height="40" fill="#00C853" stroke="#333"/>
</svg>`
},
'CDL_EVENINGSTAR': {
description: 'Evening Star - Three-candle bearish reversal: large bullish, small body (star), large bearish. Strong reversal signal.',
svg: `<svg width="100" height="80" viewBox="0 0 100 80">
<rect x="8" y="25" width="14" height="40" fill="#00C853" stroke="#333"/>
<rect x="36" y="12" width="10" height="8" fill="#888" stroke="#333"/>
<line x1="41" y1="8" x2="41" y2="12" stroke="#333" stroke-width="2"/>
<line x1="41" y1="20" x2="41" y2="28" stroke="#333" stroke-width="2"/>
<rect x="60" y="15" width="14" height="40" fill="#FF5252" stroke="#333"/>
</svg>`
},
'CDL_3WHITESOLDIERS': {
description: 'Three White Soldiers - Three consecutive long bullish candles. Very strong bullish signal, but rare pattern.',
svg: `<svg width="100" height="80" viewBox="0 0 100 80">
<rect x="8" y="45" width="14" height="25" fill="#00C853" stroke="#333"/>
<rect x="32" y="30" width="14" height="25" fill="#00C853" stroke="#333"/>
<rect x="56" y="15" width="14" height="25" fill="#00C853" stroke="#333"/>
</svg>`
},
'CDL_3BLACKCROWS': {
description: 'Three Black Crows - Three consecutive long bearish candles. Very strong bearish signal, but rare pattern.',
svg: `<svg width="100" height="80" viewBox="0 0 100 80">
<rect x="8" y="10" width="14" height="25" fill="#FF5252" stroke="#333"/>
<rect x="32" y="25" width="14" height="25" fill="#FF5252" stroke="#333"/>
<rect x="56" y="40" width="14" height="25" fill="#FF5252" stroke="#333"/>
</svg>`
},
'CDL_3INSIDE': {
description: 'Three Inside Up/Down - Three-candle reversal pattern. Harami followed by confirmation candle in reversal direction.',
svg: `<svg width="100" height="80" viewBox="0 0 100 80">
<rect x="8" y="20" width="16" height="40" fill="#FF5252" stroke="#333"/>
<rect x="34" y="30" width="10" height="15" fill="#00C853" stroke="#333"/>
<rect x="56" y="15" width="14" height="35" fill="#00C853" stroke="#333"/>
</svg>`
}
};
function showIndicatorDropdown() {
document.getElementById('indicator_dropdown').style.display = 'block';
// Don't show tooltip until hovering over an option
}
function hideIndicatorDropdown() {
setTimeout(() => {
document.getElementById('indicator_dropdown').style.display = 'none';
document.getElementById('indicator_tooltip').style.display = 'none';
}, 200);
}
function filterIndicatorTypes() {
const search = document.getElementById('newi_type_search').value.toLowerCase();
const options = document.querySelectorAll('.indicator-option');
options.forEach(opt => {
const text = opt.textContent.toLowerCase();
opt.style.display = text.includes(search) ? 'block' : 'none';
});
}
function selectIndicatorType(type) {
document.getElementById('newi_type').value = type;
document.getElementById('newi_type_search').value = type;
document.getElementById('indicator_dropdown').style.display = 'none';
document.getElementById('indicator_tooltip').style.display = 'none';
}
function showIndicatorTooltip(type) {
const info = indicatorInfo[type];
const tooltip = document.getElementById('indicator_tooltip');
const title = document.getElementById('tooltip_title');
const desc = document.getElementById('tooltip_description');
const svg = document.getElementById('tooltip_svg');
const dropdown = document.getElementById('indicator_dropdown');
if (info) {
title.textContent = type;
desc.textContent = info.description;
svg.innerHTML = info.svg || '';
} else {
title.textContent = type;
desc.textContent = 'No description available.';
svg.innerHTML = '';
}
// Get dropdown position and place tooltip to the right of it
const dropdownRect = dropdown.getBoundingClientRect();
// Position tooltip to the right of the dropdown
tooltip.style.display = 'block';
tooltip.style.position = 'fixed';
tooltip.style.left = (dropdownRect.right + 10) + 'px';
tooltip.style.top = dropdownRect.top + 'px';
// If tooltip would go off right edge of screen, put it to the left instead
const tooltipWidth = 280; // matches CSS width
if (dropdownRect.right + 10 + tooltipWidth > window.innerWidth) {
tooltip.style.left = (dropdownRect.left - tooltipWidth - 10) + 'px';
}
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
const wrapper = document.querySelector('.indicator-type-wrapper');
if (wrapper && !wrapper.contains(e.target)) {
document.getElementById('indicator_dropdown').style.display = 'none';
document.getElementById('indicator_tooltip').style.display = 'none';
}
});
</script>