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 @@
+
+