diff --git a/src/indicators.py b/src/indicators.py index db274c7..3c4b9c4 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -302,6 +302,160 @@ class MACD(Indicator): 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 indicators_registry['Volume'] = Volume indicators_registry['SMA'] = SMA @@ -313,6 +467,26 @@ indicators_registry['BOLBands'] = BolBands indicators_registry['BOL%B'] = BollingerPercentB 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: """ diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index 4bc80b7..39a939f 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -367,6 +367,14 @@ height: 500px; grid-column: 1; grid-row:4; } +#chart4{ + grid-column: 1; + grid-row:5; +} +#chart5{ + grid-column: 1; + grid-row:6; +} /***************************************************/ /****************Right Panel***********************/ diff --git a/src/static/charts.js b/src/static/charts.js index 5eff568..016f3ce 100644 --- a/src/static/charts.js +++ b/src/static/charts.js @@ -7,6 +7,7 @@ class Charts { this.chart2_id = idata.chart2_id; this.chart3_id = idata.chart3_id; this.chart4_id = idata.chart4_id; + this.chart5_id = idata.chart5_id; // Patterns chart this.trading_pair = idata.trading_pair; this.price_history = idata.price_history; /* A list of bound charts this is necessary for maintaining a dynamic @@ -68,6 +69,23 @@ class Charts { 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){ // 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 (bcl == 4) { this.bind4charts(); } + // if bound_charts has five elements in it bind them + if (bcl == 5) { this.bind5charts(); } + return; } add_to_list(chart){ @@ -233,6 +254,26 @@ class Charts { 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 setTradeMarkers(trades) { if (!this.candleSeries) { diff --git a/src/static/data.js b/src/static/data.js index 6a5e0e2..8b17656 100644 --- a/src/static/data.js +++ b/src/static/data.js @@ -8,6 +8,7 @@ class Data { this.chart2_id = 'chart2'; this.chart3_id = 'chart3'; this.chart4_id = 'chart4'; + this.chart5_id = 'chart5'; // Candlestick patterns chart /* Set into memory configuration data from the server. */ // The assets being traded. diff --git a/src/static/general.js b/src/static/general.js index b2cc62b..8849de8 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -49,6 +49,7 @@ class User_Interface { chart2_id: this.data.chart2_id, chart3_id: this.data.chart3_id, chart4_id: this.data.chart4_id, + chart5_id: this.data.chart5_id, trading_pair: this.data.trading_pair, price_history: this.data.price_history }; diff --git a/src/static/indicators.js b/src/static/indicators.js index b49f915..2e18e89 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -545,6 +545,111 @@ class Bolenger extends Indicator { } 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 { constructor(comms) { // Contains instantiated indicators. @@ -570,6 +675,8 @@ class Indicators { if (arg === 'color_1') return indicators[name].color_1 || 'red'; if (arg === 'color_2') return indicators[name].color_2 || 'white'; 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); @@ -658,12 +765,15 @@ class Indicators { let chart; // Determine which chart the indicator is on, based on its type + const indicatorType = this.i_objs[indicator].constructor.name; if (indicator.includes('RSI')) { chart = window.UI.charts.chart2; // Assume RSI is on chart2 } else if (indicator.includes('MACD')) { chart = window.UI.charts.chart3; // Assume MACD is on chart3 } else if (indicator.includes('%B') || indicator.includes('PercentB')) { chart = window.UI.charts.chart4; // %B is on chart4 + } else if (indicatorType === 'CandlestickPattern') { + chart = window.UI.charts.chart5; // Candlestick patterns on chart5 } else { 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 formData.append('indicator', JSON.stringify(selectedIndicators)); - // Show loading feedback 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 + // Hide the popup immediately const popup = document.getElementById('indicators'); if (popup) { 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 = '
Updating indicators...
'; + document.body.appendChild(overlay); + // Send form data via AJAX (fetch) fetch(form.action, { method: form.method, @@ -927,15 +1038,15 @@ class Indicators { // Handle success (you can reload the page or update the UI) window.location.reload(); } else { - // Restore button on error - submitBtn.value = originalValue; - submitBtn.disabled = false; + // Remove overlay on error + const overlay = document.getElementById('loading-overlay'); + if (overlay) overlay.remove(); alert('Failed to update indicators.'); } }).catch(error => { console.error('Error:', error); - submitBtn.value = originalValue; - submitBtn.disabled = false; + const overlay = document.getElementById('loading-overlay'); + if (overlay) overlay.remove(); alert('An error occurred while updating the indicators.'); }); } diff --git a/src/templates/index.html b/src/templates/index.html index d2fa725..8d1e0e5 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -49,7 +49,7 @@
+ grid-template-columns: 1000px 550px; grid-template-rows: 50px 500px 100px 100px 100px 100px;">

{{ title }}

@@ -61,6 +61,7 @@
+
{% include "control_panel.html" %}
diff --git a/src/templates/new_indicator_popup.html b/src/templates/new_indicator_popup.html index 42521f8..ffbcc73 100644 --- a/src/templates/new_indicator_popup.html +++ b/src/templates/new_indicator_popup.html @@ -14,14 +14,28 @@
- -
+ +
- +
+ + + + + + +
@@ -47,6 +61,8 @@
@@ -98,3 +114,294 @@
+ + +
+
+
+
+
+ + + +