diff --git a/ChanKLC.py b/ChanKLC.py index 8999da6..2f33b82 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -61,7 +61,7 @@ class ChanKLC(): #print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi']) self.klc_fx_type = klc_fx_type #self.cal_fx() - self.cal_bb_out() + #self.cal_bb_out() def add_klu(self, klu): self.klus.append(klu) def set_end_klu(self, klu): diff --git a/ChanLun.py b/ChanLun.py index cab0039..78a069d 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -84,12 +84,12 @@ class ChanLun(): def check_fx(self, klc): if klc.pre and klc.next: if klc.high > klc.pre.high and klc.high > klc.next.high: - if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: + if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0 and klc.macd > klc.macdhist: klc.set_fx(Chan_FX_TYPE.TOP) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") return Chan_FX_TYPE.TOP elif klc.low < klc.pre.low and klc.low < klc.next.low: - if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: + if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0 and klc.macd < klc.macdhist: klc.set_fx(Chan_FX_TYPE.BOTTOM) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") return Chan_FX_TYPE.BOTTOM diff --git a/web/app.py b/web/app.py index 38d4384..56bf248 100644 --- a/web/app.py +++ b/web/app.py @@ -2128,144 +2128,7 @@ def debug_replay_structure(): except Exception as e: return jsonify({'error': str(e)}) -@app.route('/api/filter_stocks', methods=['POST']) -def filter_stocks(): - """筛选满足条件的A股股票""" - try: - data = request.get_json() - start_time = data.get('start_time') - end_time = data.get('end_time') - timeframe = data.get('timeframe', '1d') - fx_strength_threshold = data.get('fx_strength_threshold', 1.0) - - if not start_time or not end_time: - return jsonify({'error': '开始时间和结束时间不能为空'}) - - # 获取所有A股股票列表,如果失败则使用热门股票作为备用 - stock_list = [] - data_source = "" - try: - stock_list = china_stock.get_stock_list() - if stock_list and len(stock_list) > 0: - data_source = "完整股票列表" - else: - raise Exception("获取到的股票列表为空") - except Exception as e: - try: - popular_stocks = china_stock.get_popular_stocks() - stock_list = [{'symbol': stock['symbol'], 'name': stock['name']} for stock in popular_stocks] - data_source = "热门股票列表" - except Exception as e2: - # 检查是否是网络连接问题 - if "timeout" in str(e).lower() or "connection" in str(e).lower() or "network" in str(e).lower(): - return jsonify({ - 'error': '网络连接超时,无法获取股票数据。请检查网络连接后重试。', - 'error_type': 'network_error', - 'suggestion': '请确保网络连接正常,或稍后重试。' - }) - else: - return jsonify({'error': f'无法获取股票列表: {str(e)}'}) - - if not stock_list: - return jsonify({ - 'error': '无法获取股票列表,请检查网络连接后重试', - 'error_type': 'network_error', - 'suggestion': '请确保网络连接正常,或稍后重试。' - }) - - results = [] - processed_count = 0 - total_count = len(stock_list) - failed_count = 0 - - for stock in stock_list: - try: - symbol = stock['symbol'] - name = stock['name'] - processed_count += 1 - - # 获取股票K线数据 - df = get_a_stock_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) - - if df is None or len(df) < 3: - failed_count += 1 - # 如果连续失败太多,可能是网络问题 - if failed_count > 10 and len(results) == 0: - return jsonify({ - 'error': '网络连接不稳定,无法获取股票数据。请检查网络连接后重试。', - 'error_type': 'network_error', - 'processed_count': processed_count, - 'failed_count': failed_count - }) - continue - - # 进行缠论分析 - analysis_result = analyze_chan(df, symbol, timeframe) - - if not analysis_result or 'klc_fx_info' not in analysis_result: - continue - - klc_fx_info = analysis_result['klc_fx_info'] - - # 检查最近2个KLC是否有满足条件的分型 - recent_klcs = klc_fx_info[-2:] if len(klc_fx_info) >= 2 else klc_fx_info - - for klc_info in recent_klcs: - fx_strength = klc_info.get('fx_strength', 0) - fx_type = klc_info.get('fx_type', 'UNKNOWN') - - # 检查是否满足条件:分型强度>=阈值 且 分型类型不为UNKNOWN - if fx_strength >= fx_strength_threshold and fx_type != 'UNKNOWN': - # 获取当前价格(最新收盘价) - current_price = df['close'].iloc[-1] if len(df) > 0 else None - fx_price = klc_info.get('price', 0) - - # 计算涨跌幅 - change_percent = 0 - if current_price and fx_price and fx_price > 0: - change_percent = ((current_price - fx_price) / fx_price) * 100 - - # 格式化分型类型显示 - fx_type_display = format_fx_type(fx_type) - - results.append({ - 'symbol': symbol, - 'name': name, - 'fx_time': klc_info.get('time', ''), - 'fx_type': fx_type_display, - 'fx_strength': fx_strength, - 'fx_price': fx_price, - 'current_price': current_price, - 'change_percent': change_percent - }) - break # 找到一个满足条件的就跳出循环 - - except Exception as e: - failed_count += 1 - continue - - # 按分型强度降序排列 - results.sort(key=lambda x: x['fx_strength'], reverse=True) - - return jsonify({ - 'results': results, - 'total_processed': processed_count, - 'total_found': len(results), - 'failed_count': failed_count, - 'data_source': data_source, - 'message': f'使用{data_source}进行筛选,共处理{processed_count}只股票,找到{len(results)}只满足条件的股票' - }) - - except Exception as e: - # 检查是否是网络连接问题 - if "timeout" in str(e).lower() or "connection" in str(e).lower() or "network" in str(e).lower(): - return jsonify({ - 'error': '网络连接超时,请检查网络连接后重试。', - 'error_type': 'network_error', - 'suggestion': '请确保网络连接正常,或稍后重试。' - }) - else: - return jsonify({'error': str(e)}) +# 已移除:/api/filter_stocks 路由 def get_uncompleted_seg_list(seg_list, client_tz): """获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段""" diff --git a/web/static/js/charts.js b/web/static/js/charts.js new file mode 100644 index 0000000..53ac532 --- /dev/null +++ b/web/static/js/charts.js @@ -0,0 +1,69 @@ +// Namespace setup +window.App = window.App || {}; +window.App.Charts = (function() { + // 依赖 Indicators + const Indicators = (window.App && window.App.Indicators) || {}; + + function addMovingAveragesToChart(candleData) { + if (!window.tvWidget || !tvWidget.mainChart || !candleData || candleData.length === 0) return; + if (!window.movingAverages) return; + if (!tvWidget.series) tvWidget.series = {}; + + if (tvWidget.series.maSeries && tvWidget.series.maSeries.length > 0) { + tvWidget.series.maSeries.forEach(series => { + try { tvWidget.mainChart.removeSeries(series); } catch(e) {} + }); + } + tvWidget.series.maSeries = []; + + window.movingAverages.forEach(maConfig => { + if (!maConfig.visible) return; + try { + const maData = Indicators.calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source); + const smoothedData = maConfig.smoothType !== 'none' ? (window.applySmoothToMA ? window.applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) : maData) : maData; + const maSeries = tvWidget.mainChart.addLineSeries({ + color: maConfig.color, + lineWidth: maConfig.lineWidth || 2, + lineStyle: maConfig.lineStyle || 0, + title: `${maConfig.type}(${maConfig.length})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true, + }); + maSeries.setData(smoothedData); + maConfig.data = smoothedData; + tvWidget.series.maSeries.push(maSeries); + } catch(e) {} + }); + } + + function addBollingerBandsToChart(candleData) { + if (!window.tvWidget || !tvWidget.mainChart || !candleData || candleData.length === 0) return; + if (!window.bollingerBands) return; + if (!tvWidget.series) tvWidget.series = {}; + + if (tvWidget.series.bbSeries && tvWidget.series.bbSeries.length > 0) { + tvWidget.series.bbSeries.forEach(series => { try { tvWidget.mainChart.removeSeries(series); } catch(e) {} }); + } + tvWidget.series.bbSeries = []; + + window.bollingerBands.forEach(bbConfig => { + if (!bbConfig.visible) return; + try { + const bbData = Indicators.calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source); + const upperSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.upperColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); + const middleSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.middleColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); + const lowerSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.lowerColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); + upperSeries.setData(bbData.map(item => ({ time: item.time, value: item.upper }))); + middleSeries.setData(bbData.map(item => ({ time: item.time, value: item.middle }))); + lowerSeries.setData(bbData.map(item => ({ time: item.time, value: item.lower }))); + bbConfig.data = bbData; + tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries); + } catch(e) {} + }); + } + + return { addMovingAveragesToChart, addBollingerBandsToChart }; +})(); + + diff --git a/web/static/js/indicators.js b/web/static/js/indicators.js new file mode 100644 index 0000000..6085042 --- /dev/null +++ b/web/static/js/indicators.js @@ -0,0 +1,99 @@ +// Namespace setup +window.App = window.App || {}; +window.App.Indicators = (function() { + function computeEMA(arr, period) { + const k = 2 / (period + 1); + const out = []; + let emaPrev = null; + for (let i = 0; i < arr.length; i++) { + const price = arr[i]; + if (price == null || !isFinite(price)) { out.push(null); continue; } + if (emaPrev == null) { + const start = Math.max(0, i - period + 1); + const windowArr = arr.slice(start, i + 1).filter(v => v != null && isFinite(v)); + const sma = windowArr.length ? windowArr.reduce((a,b)=>a+b,0)/windowArr.length : price; + emaPrev = sma; + } + const ema = price * k + emaPrev * (1 - k); + out.push(ema); + emaPrev = ema; + } + return out; + } + + function calculateMA(data, type, length, source) { + if (!data || data.length < length) return []; + const sourceData = data.map(candle => { + switch(source) { + case 'open': return candle.open; + case 'high': return candle.high; + case 'low': return candle.low; + case 'close': return candle.close; + case 'hl2': return (candle.high + candle.low) / 2; + case 'hlc3': return (candle.high + candle.low + candle.close) / 3; + case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4; + default: return candle.close; + } + }); + const result = []; + for (let i = length - 1; i < sourceData.length; i++) { + let value; + switch(type) { + case 'SMA': + value = sourceData.slice(i - length + 1, i + 1).reduce((sum, v) => sum + v, 0) / length; + break; + case 'EMA': + const multiplier = 2 / (length + 1); + if (result.length === 0) { + value = sourceData.slice(i - length + 1, i + 1).reduce((sum, v) => sum + v, 0) / length; + } else { + value = sourceData[i] * multiplier + result[result.length - 1].value * (1 - multiplier); + } + break; + case 'WMA': + let weightSum = 0; + let valueSum = 0; + for (let j = 0; j < length; j++) { + const weight = j + 1; + weightSum += weight; + valueSum += sourceData[i - length + 1 + j] * weight; + } + value = valueSum / weightSum; + break; + default: + value = sourceData[i]; + } + result.push({ time: data[i].time, value }); + } + return result; + } + + function calculateBB(data, length, upperMultiplier, lowerMultiplier, source) { + if (!data || data.length < length) return []; + const sourceData = data.map(candle => { + switch(source) { + case 'open': return candle.open; + case 'high': return candle.high; + case 'low': return candle.low; + case 'close': return candle.close; + case 'hl2': return (candle.high + candle.low) / 2; + case 'hlc3': return (candle.high + candle.low + candle.close) / 3; + case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4; + default: return candle.close; + } + }); + const result = []; + for (let i = length - 1; i < sourceData.length; i++) { + const start = Math.max(0, i - length + 1); + const slice = sourceData.slice(start, i + 1); + const avg = slice.reduce((sum, v) => sum + v, 0) / length; + const std = Math.sqrt(slice.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / length); + result.push({ time: data[i].time, upper: avg + upperMultiplier * std, middle: avg, lower: avg - lowerMultiplier * std }); + } + return result; + } + + return { computeEMA, calculateMA, calculateBB }; +})(); + + diff --git a/web/templates/index.html b/web/templates/index.html index cc330f2..e15a743 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -13,6 +13,8 @@ + +