diff --git a/DISPLAY_FIX_SUMMARY.md b/DISPLAY_FIX_SUMMARY.md deleted file mode 100644 index 9f6c91f..0000000 --- a/DISPLAY_FIX_SUMMARY.md +++ /dev/null @@ -1,102 +0,0 @@ -# 分型强度显示修复总结 - -## 问题描述 -用户反映图表上没有显示分型强度信息。 - -## 问题诊断 -1. **后端数据传递问题**: 虽然计算了分型强度,但在返回给前端的JSON数据中遗漏了强度相关字段 -2. **JSON序列化问题**: NumPy的`bool_`类型无法被JSON序列化 - -## 修复内容 - -### 1. 后端数据修复 (web/app.py) -- **主周期分型信息**: 在`klc_fx_info`中添加了遗漏的强度字段 -- **小周期分型信息**: 在`element_klc_fx_info`中添加了遗漏的强度字段 - -**修复前**: -```python -'klc_fx_info': [{ - 'time': format_time_safely(point['time'], client_tz), - 'price': point['price'], - 'fx_type': point['fx_type'], - 'is_bottom': point['is_bottom'] -} for point in analysis_result['klc_fx_info']] -``` - -**修复后**: -```python -'klc_fx_info': [{ - 'time': format_time_safely(point['time'], client_tz), - 'price': float(point['price']), - 'fx_type': point['fx_type'], - 'is_bottom': bool(point['is_bottom']), - 'fx_strength': float(point['fx_strength']), # 分型强度分数 - 'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级 - 'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型 -} for point in analysis_result['klc_fx_info']] -``` - -### 2. 数据类型转换 -- 将`numpy.bool_`转换为Python `bool` -- 将强度分数转换为`float` -- 将强度等级转换为`str` - -## 验证结果 - -### 后端API测试 ✅ -``` -=== 测试Web API分型强度数据 === -找到 154 个分型 - -分型 #1: - 强度分数: 8.79 - 强度等级: 极弱 - 是否强分型: False - -✅ 所有 154 个分型都包含完整的强度数据 -``` - -### 前端显示格式 -- **分型标记**: `分型类型(强度等级分数)`,如`TOP1(极弱8.79)` -- **视觉区分**: 强分型使用亮色+方形+大尺寸,普通分型使用圆形 -- **Tooltip详情**: 鼠标悬停显示完整强度信息 - -## 使用说明 - -1. **启动服务**: - ```bash - cd user_data/Chan/web - python app.py - ``` - -2. **访问界面**: http://localhost:8123 - -3. **查看分型强度**: - - 确保勾选"显示K线分型类型"选项 - - 图表上会显示带强度信息的分型标记 - - 鼠标悬停可查看详细强度信息 - -## 技术细节 - -### 强度评估维度 (总分100分) -- 价格差异强度: 40分 -- 突破历史点位: 20分 -- 成交量确认: 15分 -- RSI背离确认: 15分 -- MACD背离确认: 10分 - -### 强度等级划分 -- 极强: 80-100分 -- 强: 60-79分 -- 中等: 40-59分 -- 弱: 20-39分 -- 极弱: 0-19分 - -### 显示特色 -- 强分型阈值: ≥60分 -- 颜色编码: 强分型使用更亮颜色 -- 形状区分: 强分型用方形,普通分型用圆形 -- 尺寸差异: 强分型显示更大 - -## 测试工具 -`test_web_data.py` - 验证API返回的分型强度数据完整性 \ No newline at end of file diff --git a/FEATURE_COMPLETE_SUMMARY.md b/FEATURE_COMPLETE_SUMMARY.md deleted file mode 100644 index b94045f..0000000 --- a/FEATURE_COMPLETE_SUMMARY.md +++ /dev/null @@ -1,139 +0,0 @@ -# 分型强度检测功能完成总结 - -## ✅ 已完成的功能 - -### 1. **后端功能实现 (ChanKLC.py)** - -#### 核心方法添加: -- `calculate_fx_strength()`: 计算0-100分的分型强度分数 -- `get_fx_strength_level()`: 获取强度等级描述(极强/强/中等/弱/极弱) -- `is_strong_fx(threshold)`: 判断是否为强分型 - -#### 多维度强度评估体系: -- **价格差异强度 (40分)**: 分型点与相邻K线的价格差异 -- **突破历史点位 (20分)**: 是否突破前期重要高低点 -- **成交量确认 (15分)**: 分型形成时的成交量放大程度 -- **RSI背离确认 (15分)**: 价格与RSI指标的背离情况 -- **MACD背离确认 (10分)**: 价格与MACD指标的背离情况 - -#### 特征数据集成: -分型强度已自动集成到`get_feature_data()`方法中,新增8个特征: -- `klc_fx_strength`: 强度分数 (0-100) -- `klc_fx_strength_level`: 强度等级描述 -- `klc_is_strong_fx`: 是否为强分型 (1/0) -- `klc_fx_strength_extreme`: 是否为极强分型 (1/0) -- `klc_fx_strength_strong`: 是否为强分型 (1/0) -- `klc_fx_strength_medium`: 是否为中等分型 (1/0) -- `klc_fx_strength_weak`: 是否为弱分型 (1/0) -- `klc_fx_strength_very_weak`: 是否为极弱分型 (1/0) - -### 2. **前端Web显示功能 (app.py + index.html)** - -#### 后端数据传输: -- 修改`app.py`中的分型信息提取,添加强度相关数据 -- 新增字段:`fx_strength`、`fx_strength_level`、`is_strong_fx` - -#### 前端图表显示: -- 分型标记文本显示强度信息:`分型类型(强度等级分数)` -- 强分型使用更亮颜色和方形标记,普通分型使用圆形标记 -- 强分型标记尺寸更大,更容易识别 - -#### 鼠标悬停提示: -- 添加详细的tooltip显示: - - 分型类型(顶分型/底分型) - - 强度分数和等级 - - 是否为强分型 - - 价格和时间信息 -- 支持同时显示买卖点和分型信息的tooltip - -### 3. **配置系统 (fx_strength_config.py)** - -#### 预定义配置: -- **DEFAULT_CONFIG**: 默认平衡配置 -- **CONSERVATIVE_CONFIG**: 保守配置,更严格识别 -- **AGGRESSIVE_CONFIG**: 激进配置,更宽松识别 -- **TECHNICAL_CONFIG**: 技术指标重点配置 - -#### 可调参数: -- 各维度权重配置 -- 强度等级阈值设置 -- 计算参数(回看期数、放大倍数等) -- 配置验证功能 - -### 4. **示例和文档** - -#### 使用示例 (fx_strength_example.py): -- 功能演示代码 -- 筛选强分型方法 -- 统计分析功能 -- 实际应用建议 - -#### 配置示例: -- 多种预定义配置展示 -- 自定义配置方法 -- 参数调优指导 - -#### 完整文档 (README_FX_STRENGTH.md): -- 详细功能说明 -- 使用方法指导 -- 实际应用建议 -- 注意事项说明 - -### 5. **测试验证 (test_fx_strength.py)** -- 功能完整性测试 -- 特征数据验证 -- 运行状态检查 - -## 🎯 功能特色 - -### 视觉区分: -- **强分型**: 亮色 + 方形标记 + 大尺寸 -- **普通分型**: 普通色 + 圆形标记 + 标准尺寸 - -### 信息丰富: -- 标记文本包含类型和强度信息 -- 悬停显示详细分型数据 -- 多层次强度分类 - -### 高度可配置: -- 支持自定义权重和阈值 -- 多种预设配置选择 -- 灵活参数调整 - -## 🚀 使用效果 - -### 交易信号筛选: -- 只关注强度≥60的分型作为主要信号 -- 极强分型(≥80分)作为重要转折点 -- 根据强度调整仓位和止损 - -### 可视化体验: -- 图表上直观显示分型强度 -- 鼠标悬停获取详细信息 -- 强弱分型一目了然 - -### 数据分析: -- 强度特征可用于机器学习模型 -- 支持历史分型强度统计 -- 便于策略回测验证 - -## 📝 文件清单 - -1. **ChanKLC.py** - 核心实现(已修改) -2. **web/app.py** - 后端数据接口(已修改) -3. **web/templates/index.html** - 前端显示(已修改) -4. **fx_strength_config.py** - 配置系统(新建) -5. **fx_strength_example.py** - 使用示例(新建) -6. **test_fx_strength.py** - 功能测试(新建) -7. **README_FX_STRENGTH.md** - 详细文档(新建) - -## ✅ 验证结果 - -- ✅ 后端强度计算功能正常 -- ✅ 特征数据集成成功 -- ✅ 前端显示逻辑正确 -- ✅ 配置系统可用 -- ✅ 文档完整齐全 -- ✅ 测试验证通过 - -**分型强度检测功能已全面完成并可投入使用!** 🎉 \ No newline at end of file diff --git a/README_FX_STRENGTH.md b/README_FX_STRENGTH.md deleted file mode 100644 index f713abd..0000000 --- a/README_FX_STRENGTH.md +++ /dev/null @@ -1,221 +0,0 @@ -# 分型强度检测功能文档 - -## 概述 - -本功能为缠论中的顶底分型添加了强度检测机制,通过多维度分析来量化分型的可靠性和重要性。强度分数范围为0-100分,数值越高表示分型越强、越可靠。 - -## 功能特性 - -### 1. 多维度强度评估 - -分型强度通过以下5个维度进行综合评估: - -- **价格差异强度 (40分)**:分型点与相邻K线的价格差异 -- **突破历史点位 (20分)**:是否突破前期重要高低点 -- **成交量确认 (15分)**:分型形成时的成交量放大程度 -- **RSI背离确认 (15分)**:价格与RSI指标的背离情况 -- **MACD背离确认 (10分)**:价格与MACD指标的背离情况 - -### 2. 强度等级分类 - -- **极强 (80-100分)**:高可靠性分型,通常是重要转折点 -- **强 (60-79分)**:较高可靠性分型,值得重点关注 -- **中等 (40-59分)**:一般可靠性分型 -- **弱 (20-39分)**:较低可靠性分型 -- **极弱 (0-19分)**:最低可靠性分型 - -## 核心方法 - -### ChanKLC类新增方法 - -```python -def calculate_fx_strength(self): - """计算顶底分型强度,返回0-100的强度分数""" - -def get_fx_strength_level(self): - """获取分型强度等级描述字符串""" - -def is_strong_fx(self, threshold=60): - """判断是否为强分型,可自定义阈值""" -``` - -### 特征数据集成 - -分型强度自动集成到`get_feature_data()`方法中: - -```python -features = klc.get_feature_data() - -# 可获取以下分型强度相关特征: -- klc_fx_strength # 强度分数 (0-100) -- klc_fx_strength_level # 强度等级描述 -- klc_is_strong_fx # 是否为强分型 (1/0) -- klc_fx_strength_extreme # 是否为极强分型 (1/0) -- klc_fx_strength_strong # 是否为强分型 (1/0) -- klc_fx_strength_medium # 是否为中等分型 (1/0) -- klc_fx_strength_weak # 是否为弱分型 (1/0) -- klc_fx_strength_very_weak # 是否为极弱分型 (1/0) -``` - -## 使用示例 - -### 基本使用 - -```python -from ChanKLC import ChanKLC -from ChanEnum import Chan_FX_TYPE - -# 假设klc是一个已识别的分型 -if klc.fx != Chan_FX_TYPE.UNKNOWN: - strength = klc.calculate_fx_strength() - level = klc.get_fx_strength_level() - is_strong = klc.is_strong_fx() - - print(f"分型强度: {strength}分") - print(f"强度等级: {level}") - print(f"是否强分型: {is_strong}") -``` - -### 筛选强分型 - -```python -def filter_strong_fractals(klc_list, min_strength=60): - """筛选强分型""" - strong_fractals = [] - for klc in klc_list: - if klc.fx != Chan_FX_TYPE.UNKNOWN and klc.is_strong_fx(min_strength): - strong_fractals.append(klc) - return strong_fractals - -# 使用示例 -strong_fractals = filter_strong_fractals(klc_list, min_strength=70) -``` - -### 获取统计信息 - -```python -def get_fractal_statistics(klc_list): - """获取分型强度统计信息""" - stats = { - 'total_fractals': 0, - 'extreme_strength': 0, - 'strong_strength': 0, - 'medium_strength': 0, - 'weak_strength': 0, - 'very_weak_strength': 0, - 'avg_strength': 0 - } - - strengths = [] - for klc in klc_list: - if klc.fx != Chan_FX_TYPE.UNKNOWN: - stats['total_fractals'] += 1 - strength = klc.calculate_fx_strength() - strengths.append(strength) - - if strength >= 80: - stats['extreme_strength'] += 1 - elif strength >= 60: - stats['strong_strength'] += 1 - # ... 其他分类 - - if strengths: - stats['avg_strength'] = sum(strengths) / len(strengths) - - return stats -``` - -## 配置选项 - -通过`fx_strength_config.py`可以自定义分型强度检测的各项参数: - -### 预定义配置 - -- **DEFAULT_CONFIG**:默认配置,平衡各项权重 -- **CONSERVATIVE_CONFIG**:保守配置,更严格的分型识别 -- **AGGRESSIVE_CONFIG**:激进配置,更宽松的分型识别 -- **TECHNICAL_CONFIG**:技术指标配置,重视技术指标背离 - -### 自定义配置 - -```python -from fx_strength_config import FxStrengthConfig - -config = FxStrengthConfig() -config.price_difference_weight = 50 # 调整价格差异权重 -config.strong_threshold = 70 # 调整强分型阈值 -config.volume_lookback = 10 # 调整成交量回看期数 -``` - -## 强度计算详情 - -### 1. 价格差异强度 - -- 对于顶分型:计算当前高点与左右相邻点的价格差异 -- 对于底分型:计算当前低点与左右相邻点的价格差异 -- 差异越大,强度分数越高 - -### 2. 突破历史点位强度 - -- 检查是否突破前N根K线的最高/最低价 -- 突破幅度越大,强度分数越高 - -### 3. 成交量确认强度 - -- 比较当前K线成交量与前N根K线平均成交量 -- 成交量放大越多,强度分数越高 - -### 4. RSI背离确认强度 - -- 检查价格新高/新低时RSI是否出现相反走势 -- 背离程度越大,强度分数越高 - -### 5. MACD背离确认强度 - -- 检查价格新高/新低时MACD柱状图是否出现相反走势 -- 背离程度越大,强度分数越高 - -## 实际应用建议 - -### 交易策略应用 - -1. **入场信号**:只关注强度>=60的分型作为入场信号 -2. **重要转折**:极强分型(>=80分)通常预示重要转折点 -3. **止损设置**:根据分型强度调整止损距离 -4. **仓位管理**:强分型可以加大仓位,弱分型减小仓位 - -### 风险控制 - -1. **避免弱分型**:强度<40的分型建议谨慎对待 -2. **确认机制**:结合其他技术指标确认分型有效性 -3. **时间过滤**:高时间周期的强分型更可靠 -4. **市场环境**:在震荡市中提高强度阈值 - -### 机器学习特征 - -分型强度可以作为机器学习模型的重要特征: -- 直接使用强度分数作为数值特征 -- 使用强度等级分类作为分类特征 -- 结合其他技术指标构建更复杂的特征 - -## 注意事项 - -1. **数据完整性**:确保KLC对象包含完整的价格和技术指标数据 -2. **时间序列**:确保KLC之间的pre/next关系正确建立 -3. **参数调优**:根据不同市场和时间周期调整配置参数 -4. **回测验证**:在实际使用前进行充分的历史数据回测 -5. **实时更新**:分型强度会随着后续K线的变化而更新 - -## 文件说明 - -- `ChanKLC.py`:主要实现文件,包含分型强度计算逻辑 -- `fx_strength_example.py`:使用示例和功能演示 -- `fx_strength_config.py`:配置文件,支持自定义参数 -- `README_FX_STRENGTH.md`:本文档,详细说明功能特性 - -## 版本更新 - -- **v1.0**:基础分型强度检测功能 -- 支持多维度强度评估 -- 集成到特征数据系统 -- 提供配置化参数调整 \ No newline at end of file diff --git a/__pycache__/ChanBI.cpython-312.pyc b/__pycache__/ChanBI.cpython-312.pyc index 9b13e87..b454148 100644 Binary files a/__pycache__/ChanBI.cpython-312.pyc and b/__pycache__/ChanBI.cpython-312.pyc differ diff --git a/__pycache__/ChanBSP.cpython-312.pyc b/__pycache__/ChanBSP.cpython-312.pyc index cc05b4c..177daec 100644 Binary files a/__pycache__/ChanBSP.cpython-312.pyc and b/__pycache__/ChanBSP.cpython-312.pyc differ diff --git a/__pycache__/ChanCTime.cpython-312.pyc b/__pycache__/ChanCTime.cpython-312.pyc index c08bfc5..b9efe3f 100644 Binary files a/__pycache__/ChanCTime.cpython-312.pyc and b/__pycache__/ChanCTime.cpython-312.pyc differ diff --git a/__pycache__/ChanEnum.cpython-312.pyc b/__pycache__/ChanEnum.cpython-312.pyc index 60e4aad..0e8de31 100644 Binary files a/__pycache__/ChanEnum.cpython-312.pyc and b/__pycache__/ChanEnum.cpython-312.pyc differ diff --git a/__pycache__/ChanKLC.cpython-312.pyc b/__pycache__/ChanKLC.cpython-312.pyc index 34916fd..5e54994 100644 Binary files a/__pycache__/ChanKLC.cpython-312.pyc and b/__pycache__/ChanKLC.cpython-312.pyc differ diff --git a/__pycache__/ChanKLU.cpython-312.pyc b/__pycache__/ChanKLU.cpython-312.pyc index 9c66c5f..2cfd466 100644 Binary files a/__pycache__/ChanKLU.cpython-312.pyc and b/__pycache__/ChanKLU.cpython-312.pyc differ diff --git a/__pycache__/ChanLun.cpython-312.pyc b/__pycache__/ChanLun.cpython-312.pyc index e5b56c4..3a91489 100644 Binary files a/__pycache__/ChanLun.cpython-312.pyc and b/__pycache__/ChanLun.cpython-312.pyc differ diff --git a/__pycache__/ChanSBI.cpython-312.pyc b/__pycache__/ChanSBI.cpython-312.pyc index 39238c4..143d523 100644 Binary files a/__pycache__/ChanSBI.cpython-312.pyc and b/__pycache__/ChanSBI.cpython-312.pyc differ diff --git a/__pycache__/ChanSEG.cpython-312.pyc b/__pycache__/ChanSEG.cpython-312.pyc index 309ff07..dac2f10 100644 Binary files a/__pycache__/ChanSEG.cpython-312.pyc and b/__pycache__/ChanSEG.cpython-312.pyc differ diff --git a/__pycache__/ChanZS.cpython-312.pyc b/__pycache__/ChanZS.cpython-312.pyc index db18ccb..c3d3626 100644 Binary files a/__pycache__/ChanZS.cpython-312.pyc and b/__pycache__/ChanZS.cpython-312.pyc differ diff --git a/config/ChanLun_BTC_15.json b/config/ChanLun_BTC_15.json index 51a4322..bf50e85 100644 --- a/config/ChanLun_BTC_15.json +++ b/config/ChanLun_BTC_15.json @@ -5,7 +5,7 @@ "stake_amount": "unlimited", "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", - "dry_run": false, + "dry_run": true, "db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite", "dry_run_wallet": 1000, "cancel_open_orders_on_exit": true, @@ -58,12 +58,12 @@ } ], "telegram": { - "enabled": true, + "enabled": false, "token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y", "chat_id": "580807463" }, "api_server": { - "enabled": false, + "enabled": true, "listen_ip_address": "127.0.0.1", "listen_port": 8811, "verbosity": "error", diff --git a/config/ChanLun_BTC_30.json b/config/ChanLun_BTC_30.json new file mode 100644 index 0000000..5ade8e0 --- /dev/null +++ b/config/ChanLun_BTC_30.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "max_open_trades": 1, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "dry_run": true, + "db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short" : true, + "timeframe" : "1m", + "process_only_new_candles" : false, + "unfilledtimeout": { + "entry": 1, + "exit": 1, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing":{ + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8", + "secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "telegram": { + "enabled": false, + "token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y", + "chat_id": "580807463" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8812, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d", + "ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg", + "CORS_origins": [], + "username": "freqtrader", + "password": "FreqTrade007" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 2 + } +} \ No newline at end of file diff --git a/fx_strength_analysis.png b/fx_strength_analysis.png deleted file mode 100644 index 81bed6f..0000000 Binary files a/fx_strength_analysis.png and /dev/null differ diff --git a/strategies/ChanLun_BTC_15.py b/strategies/ChanLun_BTC_15.py index c39be91..0f89fc4 100644 --- a/strategies/ChanLun_BTC_15.py +++ b/strategies/ChanLun_BTC_15.py @@ -21,13 +21,13 @@ logger = logging.getLogger(__name__) # freqtrade plot-dataframe --strategy ChanLun_BTC_15 --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_15.json --timerange=20250309- # freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies -# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20250416- +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20250525- # freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_15.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405- # freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_15.json -e 200 --timerange=20250201-20250401 -# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ChanLun_SOL.json --strategy ChanLun_SOL --strategy-path ./user_data/Chan/strategies --timerange=20250101- -# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_15.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- -# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20250525- +# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_15.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_15.json --strategy ChanLun_BTC_15 --strategy-path ./user_data/Chan/strategies class ChanLun_BTC_15(IStrategy): INTERFACE_VERSION: int = 3 @@ -35,7 +35,7 @@ class ChanLun_BTC_15(IStrategy): # This attribute will be overridden if the config file contains "minimal_roi" # 30m and 1h minimal_roi = { - "0": 0.30, + "0": 0.60, "360": 0.2, "640": 0.1, "1200": 0 @@ -61,7 +61,7 @@ class ChanLun_BTC_15(IStrategy): "3600": 0 } can_short = True - lev = 20.0 + lev = 50.0 stoploss = -0.3 trailing_stop = False trailing_stop_positive = 0.025 @@ -147,14 +147,35 @@ class ChanLun_BTC_15(IStrategy): # 填充缺失值(前N根K线) df['volume_ratio'] = df['volume_ratio'].fillna(1.0) return df['volume_ratio'] + def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, + entry_tag: str | None, side: str, **kwargs) -> float: + new_entryprice = proposed_rate + if trade: + if trade.is_short: + new_entryprice = proposed_rate - 50 + else: + new_entryprice = proposed_rate + 50 + return new_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: + new_exitprice = proposed_rate + if trade: + if trade.is_short: + new_exitprice = proposed_rate + 50 + else: + new_exitprice = proposed_rate - 50 + return new_exitprice + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time5) fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time5) dataframe.loc[ ( #(dataframe['state'] == "-30") - (dataframe[state_str].shift(self.time5*2) > 1.0) & - (dataframe[fx_str].shift(self.time5*2) == -1) + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == -1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") @@ -164,8 +185,8 @@ class ChanLun_BTC_15(IStrategy): dataframe.loc[ ( #(dataframe['state'] == "-30") - (dataframe[state_str].shift(self.time5*2) > 1.0) & - (dataframe[fx_str].shift(self.time5*2) == 1) + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == 1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") @@ -179,8 +200,8 @@ class ChanLun_BTC_15(IStrategy): dataframe.loc[ ( #(dataframe['state']== "30") - (dataframe[state_str].shift(self.time5*2) > 1.0) & - (dataframe[fx_str].shift(self.time5*2) == 1) + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == 1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") ), @@ -188,8 +209,8 @@ class ChanLun_BTC_15(IStrategy): dataframe.loc[ ( #(dataframe['state']== "30") - (dataframe[state_str].shift(self.time5*2) > 1.0) & - (dataframe[fx_str].shift(self.time5*2) == -1) + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == -1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") ), diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py new file mode 100644 index 0000000..4f12a3e --- /dev/null +++ b/strategies/ChanLun_BTC_30.py @@ -0,0 +1,225 @@ +# --- Do not remove these libs --- +from statistics import median +from freqtrade.strategy import IStrategy +import sys +import os +# 添加父目录到系统路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ChanLun import ChanLun +from ChanLun_Classifier import ChanLunClassifier +from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +from datetime import datetime, timedelta +from freqtrade.persistence import Trade +from typing import Optional +import logging +logger = logging.getLogger(__name__) +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_BTC_30 --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies --timerange=20250520- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250401 + +# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies --timerange=20250101- +# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies + +class ChanLun_BTC_30(IStrategy): + INTERFACE_VERSION: int = 3 + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + # 30m and 1h + minimal_roi = { + "0": 0.60, + "360": 0.2, + "640": 0.1, + "1200": 0 + } + # 5m and 15m + minimal_roi_1 = { + "0": 0.1, + "60": 0.05, + "120": 0.02, + "240": 0 + } + # 15m and 30m + minimal_roi_1 = { + "0": 0.1, + "240": 0.05, + "480": 0.03, + "600": 0 + } + minimal_roi_2 = { + "0": 0.10, + "1200": 0.05, + "2400": 0.025, + "3600": 0 + } + can_short = True + lev = 50.0 + stoploss = -0.3 + trailing_stop = False + trailing_stop_positive = 0.025 + trailing_stop_positive_offset = 0.045 + trailing_only_offset_is_reached = False + + position_adjustment_enable = True + startup_candle_count = 600 + + time5 = 5 + time15 = 15 + time30 = 30 + time60 = 60 + time4h = 240 + time5 = 30 + last_time = datetime.now() + chan = ChanLun() + classifier = ChanLunClassifier(None) + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # resample our dataframes + dataframe_5 = resample_to_interval(dataframe, self.get_ticker_indicator() * 5) + dataframe_15 = resample_to_interval(dataframe, self.get_ticker_indicator() * 15) + dataframe_30 = resample_to_interval(dataframe, self.get_ticker_indicator() * 30) + dataframe_60 = resample_to_interval(dataframe, self.get_ticker_indicator() * 60) + dataframe_4h = resample_to_interval(dataframe, self.get_ticker_indicator() * 240) + + #dataframe_1d = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') + #dataframe_1w = resample_to_interval(dataframe_1d, self.get_ticker_indicator() * 10080) + #dataframe_1m = resample_to_interval(dataframe_1d, self.get_ticker_indicator() * 43200) + + dataframe_1d = resample_to_interval(dataframe, self.get_ticker_indicator() * 1440) + #dataframe_1w = resample_to_interval(dataframe, self.get_ticker_indicator() * 10080) + #dataframe_1m = resample_to_interval(dataframe, self.get_ticker_indicator() * 43200) + dataframe = self.add_indicators(dataframe) + dataframe_5 = self.add_indicators(dataframe_5) + dataframe_30 = self.add_indicators(dataframe_30) + dataframe_60 = self.add_indicators(dataframe_60) + dataframe_4h = self.add_indicators(dataframe_4h) + dataframe_1d = self.add_indicators(dataframe_1d) + #self.chan.plot_dual(dataframe_5, dataframe_30) + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + + state_list, fx_list = self.chan.get_klc_strength_list(dataframe_30) + dataframe_30['state'] = state_list + dataframe_30['fx'] = fx_list + klc_list = self.chan.get_klc_list(dataframe_30) + bi_list = self.chan.cal_bi_list(klc_list) + if self.last_time + timedelta(minutes=1) < datetime.now(): + print(state_list[-1], state_list[-2], state_list[-3], state_list[-4], state_list[-5]) + print(fx_list[-1], fx_list[-2], fx_list[-3], fx_list[-4], fx_list[-5]) + print(klc_list[-1].klc_fx_type, klc_list[-2].klc_fx_type, klc_list[-3].klc_fx_type, klc_list[-4].klc_fx_type, klc_list[-5].klc_fx_type) + print("-------------------------------------------------------------------------------") + self.last_time = datetime.now() + #dataframe = resampled_merge(dataframe, dataframe_5) + dataframe = resampled_merge(dataframe, dataframe_30) + #dataframe = resampled_merge(dataframe, dataframe_30) + #dataframe = resampled_merge(dataframe, dataframe_60) + #dataframe = resampled_merge(dataframe, dataframe_4h) + return dataframe + + def add_indicators(self, df): + fast = 8 + slow = 16 + period = 6 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ma5'] = ta.MA(df, timeperiod=5) + df['ma10'] = ta.MA(df, timeperiod=10) + df['ma30'] = ta.EMA(df, timeperiod=30) + df['ma250'] = ta.MA(df, timeperiod=250) + df['rsi'] = ta.RSI(df, timeperiod=14) + df['volume_ratio'] = self.cal_volume_ratio(df) + return df + def cal_volume_ratio(self, dataframe, window=10): + df = dataframe.copy() + # 计算过去N根K线的平均成交量 + df['avg_volume'] = df['volume'].rolling(window=window).mean() + # 计算量比 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + return df['volume_ratio'] + def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, + entry_tag: str | None, side: str, **kwargs) -> float: + new_entryprice = proposed_rate + if trade: + if trade.is_short: + new_entryprice = proposed_rate - 50 + else: + new_entryprice = proposed_rate + 50 + return new_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: + new_exitprice = proposed_rate + if trade: + if trade.is_short: + new_exitprice = proposed_rate + 50 + else: + new_exitprice = proposed_rate - 50 + return new_exitprice + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time5) + fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time5) + dataframe.loc[ + ( + #(dataframe['state'] == "-30") + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == -1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") + #(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + #(dataframe['state'] == "-30") + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == 1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") + #(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_chan') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time5) + fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time5) + dataframe.loc[ + ( + #(dataframe['state']== "30") + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == 1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") + ), + ['exit_long', 'exit_tag']] = (1, 'long_close_signal_chan') + dataframe.loc[ + ( + #(dataframe['state']== "30") + (dataframe[state_str].shift(self.time5) > 1.0) & + (dataframe[fx_str].shift(self.time5) == -1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") + ), + ['exit_short', 'exit_tag']] = (1, 'short_close_signal_chan') + return dataframe + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + return self.lev + + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) \ No newline at end of file diff --git a/test_a_stock.py b/test_a_stock.py deleted file mode 100644 index bf01ef1..0000000 --- a/test_a_stock.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -A股数据获取测试脚本 -""" - -import sys -import os -sys.path.append(os.path.join(os.path.dirname(__file__), 'web')) - -from web.cn_stock_data import ChinaStockData -import pandas as pd - -def test_a_stock_data(): - """测试A股数据获取功能""" - print("开始测试A股数据获取功能...") - - # 初始化A股数据获取器 - china_stock = ChinaStockData() - - # 测试获取热门股票列表 - print("\n1. 测试获取热门股票列表:") - popular_stocks = china_stock.get_popular_stocks() - print(f"热门股票数量: {len(popular_stocks)}") - for i, stock in enumerate(popular_stocks[:5]): - print(f" {i+1}. {stock['symbol']} - {stock['name']}") - - # 测试获取股票K线数据 - print("\n2. 测试获取股票K线数据:") - test_symbols = ['000001', '600519', '000858'] # 平安银行、贵州茅台、五粮液 - - for symbol in test_symbols: - print(f"\n测试股票: {symbol}") - - # 测试日线数据 - print(" 获取日线数据...") - try: - df_daily = china_stock.get_kl_data(symbol, '1d', limit=100) - if df_daily is not None: - print(f" 成功获取 {len(df_daily)} 条日线数据") - print(f" 时间范围: {df_daily['date'].min()} 到 {df_daily['date'].max()}") - print(f" 最新价格: {df_daily['close'].iloc[-1]:.2f}") - else: - print(" 获取日线数据失败") - except Exception as e: - print(f" 获取日线数据出错: {e}") - - # 测试分钟数据 - print(" 获取5分钟数据...") - try: - df_5m = china_stock.get_kl_data(symbol, '5m', limit=50) - if df_5m is not None: - print(f" 成功获取 {len(df_5m)} 条5分钟数据") - print(f" 时间范围: {df_5m['date'].min()} 到 {df_5m['date'].max()}") - else: - print(" 获取5分钟数据失败") - except Exception as e: - print(f" 获取5分钟数据出错: {e}") - - # 测试获取股票列表 - print("\n3. 测试获取股票列表:") - try: - stock_list = china_stock.get_stock_list() - if stock_list: - print(f"成功获取 {len(stock_list)} 只股票") - print("前5只股票:") - for i, stock in enumerate(stock_list[:5]): - print(f" {i+1}. {stock['symbol']} - {stock['name']} - 价格: {stock['price']} - 涨跌幅: {stock['change_pct']}%") - else: - print("获取股票列表失败") - except Exception as e: - print(f"获取股票列表出错: {e}") - - print("\nA股数据获取测试完成!") - -if __name__ == '__main__': - test_a_stock_data() \ No newline at end of file diff --git a/test_batch_data.py b/test_batch_data.py deleted file mode 100644 index 6f344a2..0000000 --- a/test_batch_data.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试分批次数据获取功能 -验证A股和加密货币数据的大时间范围获取 -""" - -import sys -import os -sys.path.append('web') - -from datetime import datetime, timedelta -from cn_stock_data import ChinaStockData -import ccxt - -def test_a_stock_batch_data(): - """测试A股分批次数据获取""" - print("=== 测试A股分批次数据获取 ===") - - china_stock = ChinaStockData() - - # 测试获取更长时间范围的数据 - end_date = datetime.now() - start_date = end_date - timedelta(days=180) # 6个月数据 - - print(f"测试时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}") - - # 测试不同时间周期 - test_cases = [ - ('600519', '1d', '日线数据'), - ('600519', '1h', '1小时数据'), - ('600519', '15m', '15分钟数据'), - ] - - for symbol, timeframe, description in test_cases: - print(f"\n测试 {description}: {symbol} {timeframe}") - - try: - df = china_stock.get_kl_data( - symbol=symbol, - timeframe=timeframe, - start_date=start_date.strftime('%Y-%m-%d'), - end_date=end_date.strftime('%Y-%m-%d'), - limit=5000 - ) - - if df is not None: - print(f"✅ 成功获取 {len(df)} 条记录") - print(f" 时间范围: {df['date'].min()} 到 {df['date'].max()}") - print(f" 数据列: {list(df.columns)}") - else: - print(f"❌ 获取失败") - - except Exception as e: - print(f"❌ 错误: {e}") - -def test_crypto_batch_data(): - """测试加密货币分批次数据获取""" - print("\n=== 测试加密货币分批次数据获取 ===") - - # 初始化交易所 - exchange = ccxt.binance({ - 'enableRateLimit': True, - }) - - # 测试获取更长时间范围的数据 - end_time = datetime.now() - start_time = end_time - timedelta(days=30) # 30天数据 - - print(f"测试时间范围: {start_time} 到 {end_time}") - - # 转换为时间戳 - start_timestamp = int(start_time.timestamp() * 1000) - end_timestamp = int(end_time.timestamp() * 1000) - - # 测试不同时间周期 - test_cases = [ - ('BTC/USDT:USDT', '1d', '日线数据'), - ('BTC/USDT:USDT', '1h', '1小时数据'), - ('BTC/USDT:USDT', '5m', '5分钟数据'), - ] - - for symbol, timeframe, description in test_cases: - print(f"\n测试 {description}: {symbol} {timeframe}") - - try: - # 模拟分批次获取逻辑 - all_ohlcv = [] - current_since = start_timestamp - request_count = 0 - max_requests = 10 - - batch_size = 500 if timeframe in ['1m', '5m'] else 1000 - - while request_count < max_requests and current_since < end_timestamp: - request_count += 1 - print(f" 批次 {request_count}: 获取数据...") - - ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size) - - if not ohlcv or len(ohlcv) == 0: - break - - all_ohlcv.extend(ohlcv) - last_timestamp = ohlcv[-1][0] - - if last_timestamp >= end_timestamp: - break - - if len(ohlcv) < batch_size: - break - - current_since = last_timestamp + 1 - - # 防止请求过频 - import time - time.sleep(0.3) - - if all_ohlcv: - print(f"✅ 成功获取 {len(all_ohlcv)} 条记录 (共 {request_count} 个批次)") - - # 时间范围检查 - first_time = datetime.fromtimestamp(all_ohlcv[0][0] / 1000) - last_time = datetime.fromtimestamp(all_ohlcv[-1][0] / 1000) - print(f" 时间范围: {first_time} 到 {last_time}") - else: - print(f"❌ 获取失败") - - except Exception as e: - print(f"❌ 错误: {e}") - -def test_data_quality(): - """测试数据质量""" - print("\n=== 测试数据质量 ===") - - china_stock = ChinaStockData() - - # 获取一小段数据进行质量检查 - df = china_stock.get_kl_data( - symbol='600519', - timeframe='1d', - limit=100 - ) - - if df is not None: - print(f"数据行数: {len(df)}") - print(f"数据列: {list(df.columns)}") - - # 检查缺失值 - missing_values = df.isnull().sum() - print(f"缺失值统计:") - for col, count in missing_values.items(): - if count > 0: - print(f" {col}: {count}") - - # 检查数据类型 - print(f"数据类型:") - for col, dtype in df.dtypes.items(): - print(f" {col}: {dtype}") - - # 检查时间连续性 - if len(df) > 1: - time_diffs = df['date'].diff().dropna() - print(f"时间间隔统计:") - print(f" 最小间隔: {time_diffs.min()}") - print(f" 最大间隔: {time_diffs.max()}") - print(f" 平均间隔: {time_diffs.mean()}") - - # 检查价格合理性 - price_cols = ['open', 'high', 'low', 'close'] - for col in price_cols: - if col in df.columns: - print(f"{col} 价格范围: {df[col].min():.2f} - {df[col].max():.2f}") - - print("✅ 数据质量检查完成") - else: - print("❌ 无法获取数据进行质量检查") - -if __name__ == '__main__': - print("开始测试分批次数据获取功能...\n") - - # 测试A股数据 - test_a_stock_batch_data() - - # 测试加密货币数据 - test_crypto_batch_data() - - # 测试数据质量 - test_data_quality() - - print("\n测试完成!") \ No newline at end of file diff --git a/test_fx_strength.py b/test_fx_strength.py deleted file mode 100644 index b464ad2..0000000 --- a/test_fx_strength.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试分型强度检测功能 -""" - -from ChanKLC import ChanKLC -import ChanKLU -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR -import requests -import json -import numpy as np -import matplotlib.pyplot as plt -from collections import Counter - -def test_fx_strength(): - """测试分型强度检测功能""" - print('=== 分型强度检测功能测试 ===') - - # 创建一个简单的测试KLU,使用正确的构造函数参数 - klu = ChanKLU.ChanKLU( - time='2024-01-01 10:00:00', - open=100.0, - high=105.0, - low=98.0, - close=103.0, - volume=1000 - ) - klu.rsi = 65.0 - klu.volume_ratio = 1.2 - klu.macdhist = 0.5 - - # 创建KLC对象 - klc = ChanKLC(klu, 1, Chan_KLINE_DIR.UP) - klc.fx = Chan_FX_TYPE.TOP - - # 测试强度计算 - strength = klc.calculate_fx_strength() - level = klc.get_fx_strength_level() - is_strong = klc.is_strong_fx() - - print(f'分型强度分数: {strength}') - print(f'分型强度等级: {level}') - print(f'是否强分型: {is_strong}') - - # 测试特征数据集成 - features = klc.get_feature_data() - fx_features = {k: v for k, v in features.items() if 'fx_strength' in k} - print('\n分型强度相关特征:') - for key, value in fx_features.items(): - print(f' {key}: {value}') - - print('\n✅ 分型强度检测功能正常工作!') - return True - -def test_fx_strength_distribution(): - """测试分型强度分布情况""" - - print("=== 分型强度分布分析 ===") - - # 请求API数据 - url = "http://localhost:8123/api/analyze" - params = { - 'symbol': 'SOL/USDT:USDT', - 'timeframe': '5m', - 'timezone': 'Asia/Shanghai' - } - - try: - response = requests.get(url, params=params) - response.raise_for_status() - data = response.json() - except Exception as e: - print(f"❌ 请求API失败: {e}") - return - - # 提取分型强度数据 - fx_strengths = [] - fx_levels = [] - top_strengths = [] - bottom_strengths = [] - - for fx in data.get('klc_fx_info', []): - strength = fx.get('fx_strength', 0) - level = fx.get('fx_strength_level', 'Unknown') - is_bottom = fx.get('is_bottom_fx', False) - - fx_strengths.append(strength) - fx_levels.append(level) - - if is_bottom: - bottom_strengths.append(strength) - else: - top_strengths.append(strength) - - # 统计分析 - if fx_strengths: - print(f"\n📊 基础统计:") - print(f"总分型数量: {len(fx_strengths)}") - print(f"平均强度: {np.mean(fx_strengths):.2f}") - print(f"强度中位数: {np.median(fx_strengths):.2f}") - print(f"强度标准差: {np.std(fx_strengths):.2f}") - print(f"最高强度: {np.max(fx_strengths):.2f}") - print(f"最低强度: {np.min(fx_strengths):.2f}") - - print(f"\n🔝 顶分型统计:") - if top_strengths: - print(f"数量: {len(top_strengths)}") - print(f"平均强度: {np.mean(top_strengths):.2f}") - print(f"最高强度: {np.max(top_strengths):.2f}") - - print(f"\n🔻 底分型统计:") - if bottom_strengths: - print(f"数量: {len(bottom_strengths)}") - print(f"平均强度: {np.mean(bottom_strengths):.2f}") - print(f"最高强度: {np.max(bottom_strengths):.2f}") - - # 强度等级分布 - print(f"\n📈 强度等级分布:") - level_counts = Counter(fx_levels) - for level, count in level_counts.items(): - percentage = (count / len(fx_levels)) * 100 - print(f"{level}: {count} ({percentage:.1f}%)") - - # 强度区间分布 - print(f"\n📊 强度区间分布:") - ranges = [ - (0, 20, "极弱 (0-20)"), - (20, 40, "弱 (20-40)"), - (40, 60, "中等 (40-60)"), - (60, 80, "强 (60-80)"), - (80, 100, "极强 (80-100)") - ] - - for min_val, max_val, label in ranges: - count = sum(1 for s in fx_strengths if min_val <= s < max_val) - percentage = (count / len(fx_strengths)) * 100 - print(f"{label}: {count} ({percentage:.1f}%)") - - # 找出最强和最弱的分型 - print(f"\n⭐ 最强分型 (Top 5):") - sorted_fx = sorted(data.get('klc_fx_info', []), - key=lambda x: x.get('fx_strength', 0), - reverse=True)[:5] - - for i, fx in enumerate(sorted_fx, 1): - fx_type = "底分型" if fx.get('is_bottom_fx', False) else "顶分型" - print(f" {i}. {fx.get('time', 'N/A')} - {fx_type} - 强度: {fx.get('fx_strength', 0):.2f} - 等级: {fx.get('fx_strength_level', 'N/A')}") - - print(f"\n💔 最弱分型 (Bottom 5):") - weakest_fx = sorted(data.get('klc_fx_info', []), - key=lambda x: x.get('fx_strength', 0))[:5] - - for i, fx in enumerate(weakest_fx, 1): - fx_type = "底分型" if fx.get('is_bottom_fx', False) else "顶分型" - print(f" {i}. {fx.get('time', 'N/A')} - {fx_type} - 强度: {fx.get('fx_strength', 0):.2f} - 等级: {fx.get('fx_strength_level', 'N/A')}") - - # 生成直方图 - try: - plt.figure(figsize=(12, 8)) - - # 主强度分布图 - plt.subplot(2, 2, 1) - plt.hist(fx_strengths, bins=20, alpha=0.7, color='blue', edgecolor='black') - plt.title('分型强度分布') - plt.xlabel('强度分数') - plt.ylabel('频次') - plt.axvline(np.mean(fx_strengths), color='red', linestyle='--', label=f'平均值: {np.mean(fx_strengths):.2f}') - plt.legend() - - # 顶分型 vs 底分型对比 - plt.subplot(2, 2, 2) - if top_strengths and bottom_strengths: - plt.hist([top_strengths, bottom_strengths], bins=15, alpha=0.7, - label=['顶分型', '底分型'], color=['red', 'green']) - plt.title('顶分型 vs 底分型强度对比') - plt.xlabel('强度分数') - plt.ylabel('频次') - plt.legend() - - # 强度等级饼图 - plt.subplot(2, 2, 3) - if level_counts: - labels = list(level_counts.keys()) - sizes = list(level_counts.values()) - colors = ['red', 'orange', 'yellow', 'lightgreen', 'green'][:len(labels)] - plt.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%') - plt.title('强度等级分布') - - # 时间序列图 - plt.subplot(2, 2, 4) - x_vals = range(len(fx_strengths)) - colors = ['red' if not fx.get('is_bottom_fx', False) else 'green' - for fx in data.get('klc_fx_info', [])] - plt.scatter(x_vals, fx_strengths, c=colors, alpha=0.6) - plt.title('分型强度时间序列 (红=顶分型, 绿=底分型)') - plt.xlabel('分型序号') - plt.ylabel('强度分数') - - plt.tight_layout() - plt.savefig('user_data/Chan/fx_strength_analysis.png', dpi=300, bbox_inches='tight') - print(f"\n📈 图表已保存到: user_data/Chan/fx_strength_analysis.png") - - except ImportError: - print("\n📈 matplotlib 未安装,跳过图表生成") - except Exception as e: - print(f"\n❌ 生成图表失败: {e}") - - else: - print("❌ 未找到分型强度数据") - -if __name__ == "__main__": - test_fx_strength() - test_fx_strength_distribution() \ No newline at end of file diff --git a/test_web_data.py b/test_web_data.py deleted file mode 100644 index 0bfb9d2..0000000 --- a/test_web_data.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试web接口返回的分型强度数据 -""" - -import requests -import json -import sys -import os - -# 添加父目录到系统路径以便导入模块 -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -def test_web_api(): - """测试web API接口返回的分型强度数据""" - - print('=== 测试Web API分型强度数据 ===') - - # 构建请求URL - base_url = "http://localhost:8123" - endpoint = "/api/analyze" - - params = { - 'symbol': 'SOL/USDT:USDT', - 'timeframe': '5m', - 'timezone': 'Asia/Shanghai' - } - - try: - print(f"发送请求到: {base_url}{endpoint}") - print(f"参数: {params}") - - # 发送请求 - response = requests.get(f"{base_url}{endpoint}", params=params, timeout=30) - - if response.status_code == 200: - data = response.json() - - # 检查是否有分型信息 - if 'klc_fx_info' in data: - fx_info = data['klc_fx_info'] - print(f"\n找到 {len(fx_info)} 个分型") - - # 显示前3个分型的详细信息 - for i, fx in enumerate(fx_info[:3]): - print(f"\n分型 #{i+1}:") - print(f" 时间: {fx.get('time', '无')}") - print(f" 价格: {fx.get('price', '无')}") - print(f" 分型类型: {fx.get('fx_type', '无')}") - print(f" 是否底分型: {fx.get('is_bottom', '无')}") - print(f" 强度分数: {fx.get('fx_strength', '缺失!')}") - print(f" 强度等级: {fx.get('fx_strength_level', '缺失!')}") - print(f" 是否强分型: {fx.get('is_strong_fx', '缺失!')}") - - # 检查强度数据是否完整 - missing_strength_count = 0 - for fx in fx_info: - if 'fx_strength' not in fx or 'fx_strength_level' not in fx or 'is_strong_fx' not in fx: - missing_strength_count += 1 - - if missing_strength_count == 0: - print(f"\n✅ 所有 {len(fx_info)} 个分型都包含完整的强度数据") - else: - print(f"\n❌ 有 {missing_strength_count} 个分型缺少强度数据") - - else: - print("\n❌ 响应中未找到分型信息 (klc_fx_info)") - - # 检查小周期分型信息 - if 'element_klc_fx_info' in data: - element_fx_info = data['element_klc_fx_info'] - print(f"\n找到 {len(element_fx_info)} 个小周期分型") - - # 检查小周期强度数据 - missing_element_strength_count = 0 - for fx in element_fx_info: - if 'fx_strength' not in fx or 'fx_strength_level' not in fx or 'is_strong_fx' not in fx: - missing_element_strength_count += 1 - - if missing_element_strength_count == 0: - print(f"✅ 所有 {len(element_fx_info)} 个小周期分型都包含完整的强度数据") - else: - print(f"❌ 有 {missing_element_strength_count} 个小周期分型缺少强度数据") - - else: - print(f"❌ 请求失败,状态码: {response.status_code}") - print(f"响应内容: {response.text}") - - except requests.exceptions.ConnectionError: - print("❌ 无法连接到服务器,请确保web服务正在运行 (python web/app.py)") - except Exception as e: - print(f"❌ 测试过程中出错: {e}") - -def print_usage(): - """打印使用说明""" - print("\n=== 使用说明 ===") - print("1. 确保web服务正在运行:") - print(" cd user_data/Chan/web") - print(" python app.py") - print("\n2. 然后运行此测试脚本:") - print(" python test_web_data.py") - print("\n3. 检查控制台输出,确认分型强度数据是否正确返回") - -if __name__ == "__main__": - test_web_api() - print_usage() \ No newline at end of file diff --git a/web/app.py b/web/app.py index eab040e..048d789 100644 --- a/web/app.py +++ b/web/app.py @@ -225,17 +225,35 @@ def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time if start_time: try: + # 尝试解析时间戳(毫秒) start_timestamp = int(start_time) start_date = datetime.fromtimestamp(start_timestamp / 1000).strftime('%Y-%m-%d') - except: - start_date = start_time + except (ValueError, TypeError): + # 如果不是时间戳,尝试解析datetime-local格式 (YYYY-MM-DDTHH:MM) + try: + if 'T' in str(start_time): + # datetime-local格式:2025-05-19T06:07 + start_date = str(start_time).split('T')[0] # 只取日期部分 + else: + start_date = str(start_time) + except: + start_date = start_time if end_time: try: + # 尝试解析时间戳(毫秒) end_timestamp = int(end_time) end_date = datetime.fromtimestamp(end_timestamp / 1000).strftime('%Y-%m-%d') - except: - end_date = end_time + except (ValueError, TypeError): + # 如果不是时间戳,尝试解析datetime-local格式 + try: + if 'T' in str(end_time): + # datetime-local格式:2025-05-26T06:07 + end_date = str(end_time).split('T')[0] # 只取日期部分 + else: + end_date = str(end_time) + except: + end_date = end_time # 如果用户指定了时间范围,优先获取该范围内的所有数据 actual_limit = limit @@ -328,22 +346,51 @@ def analyze_chan(df): klc_fx_info = [] for klc in klc_list: if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - # 计算分型强度 - #fx_strength = klc.calculate_fx_strength() - fx_strength = klc.cal_fx_strength() - fx_strength_level = klc.get_fx_strength_level() - is_strong_fx = klc.is_strong_fx() - if fx_strength < 1: + try: + # 计算分型强度 fx_strength = 0 - klc_fx_info.append({ - 'time': klc.end_time, - 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, - 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), - 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, - 'fx_strength': fx_strength, # 分型强度分数 (0-100) - 'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱) - 'is_strong_fx': is_strong_fx # 是否为强分型 - }) + fx_strength_level = "" + is_strong_fx = False + + # 尝试调用分型强度计算方法 + if hasattr(klc, 'cal_fx_strength'): + fx_strength = klc.cal_fx_strength() + elif hasattr(klc, 'calculate_fx_strength'): + fx_strength = klc.calculate_fx_strength() + + # 尝试获取分型强度等级 + if hasattr(klc, 'get_fx_strength_level'): + fx_strength_level = klc.get_fx_strength_level() + + # 尝试判断是否为强分型 + if hasattr(klc, 'is_strong_fx'): + is_strong_fx = klc.is_strong_fx() + + # 如果分型强度小于1,设为0 + if fx_strength < 1: + fx_strength = 0 + + klc_fx_info.append({ + 'time': klc.end_time, + 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, + 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), + 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, + 'fx_strength': fx_strength, # 分型强度分数 (0-100) + 'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱) + 'is_strong_fx': is_strong_fx # 是否为强分型 + }) + except Exception as e: + print(f"处理KLC分型信息时出错: {e}") + # 如果出错,仍然添加基本信息,但分型强度为0 + klc_fx_info.append({ + 'time': klc.end_time, + 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, + 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), + 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, + 'fx_strength': 0, + 'fx_strength_level': "", + 'is_strong_fx': False + }) return { 'klc_list': klc_list, @@ -766,5 +813,175 @@ def search_stock(): 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: + print("正在获取完整股票列表...") + stock_list = china_stock.get_stock_list() + if stock_list and len(stock_list) > 0: + print(f"成功获取完整股票列表: {len(stock_list)} 只股票") + data_source = "完整股票列表" + else: + raise Exception("获取到的股票列表为空") + except Exception as e: + print(f"获取完整股票列表失败: {e}") + print("使用热门股票列表作为备用...") + try: + popular_stocks = china_stock.get_popular_stocks() + stock_list = [{'symbol': stock['symbol'], 'name': stock['name']} for stock in popular_stocks] + print(f"使用热门股票列表: {len(stock_list)} 只股票") + data_source = "热门股票列表" + except Exception as e2: + print(f"获取热门股票列表也失败: {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 + + print(f"开始筛选股票,总数: {total_count}, 时间范围: {start_time} 到 {end_time}, 周期: {timeframe}") + + for stock in stock_list: + try: + symbol = stock['symbol'] + name = stock['name'] + processed_count += 1 + + # 每处理20只股票打印一次进度 + if processed_count % 20 == 0: + print(f"已处理 {processed_count}/{total_count} 只股票,成功: {len(results)}, 失败: {failed_count}") + + # 获取股票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: + print(f"连续失败 {failed_count} 次,可能是网络问题") + return jsonify({ + 'error': '网络连接不稳定,无法获取股票数据。请检查网络连接后重试。', + 'error_type': 'network_error', + 'processed_count': processed_count, + 'failed_count': failed_count + }) + continue + + # 进行缠论分析 + analysis_result = analyze_chan(df) + + 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: + print(f"处理股票 {symbol} 时出错: {str(e)}") + failed_count += 1 + continue + + print(f"筛选完成,共找到 {len(results)} 只满足条件的股票") + + # 按分型强度降序排列 + 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: + print(f"筛选股票时发生错误: {str(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)}) + +def format_fx_type(fx_type): + """格式化分型类型显示""" + fx_type_map = { + 'TOP1': '顶分型1', + 'TOP2': '顶分型2', + 'TOP3': '顶分型3', + 'BOTTOM1': '底分型1', + 'BOTTOM2': '底分型2', + 'BOTTOM3': '底分型3', + 'TOP': '顶分型', + 'BOTTOM': '底分型' + } + return fx_type_map.get(fx_type, fx_type) + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=8123) \ No newline at end of file diff --git a/web/cn_stock_data.py b/web/cn_stock_data.py index 0960e03..cb916f9 100644 --- a/web/cn_stock_data.py +++ b/web/cn_stock_data.py @@ -21,26 +21,59 @@ class ChinaStockData: def get_stock_list(self): """获取A股股票列表""" try: - # 获取沪深A股实时行情 - stock_info = ak.stock_zh_a_spot_em() + import requests + # 设置较短的超时时间,避免长时间等待 + import akshare as ak + + print("正在获取A股股票列表...") + + # 尝试获取沪深A股实时行情,设置超时时间 + try: + # 临时设置requests的默认超时 + original_timeout = getattr(requests, 'timeout', None) + requests.timeout = 10 # 10秒超时 + + stock_info = ak.stock_zh_a_spot_em() + + # 恢复原始超时设置 + if original_timeout: + requests.timeout = original_timeout + else: + delattr(requests, 'timeout') + + except Exception as network_error: + print(f"网络请求失败: {network_error}") + # 网络失败时返回空列表,让调用方使用备用方案 + return [] + + if stock_info is None or len(stock_info) == 0: + print("获取到的股票数据为空") + return [] + # 增加到前2000只股票,提供更多选择 stock_list = [] for index, row in stock_info.head(2000).iterrows(): - # 过滤掉ST股票和停牌股票 - stock_name = str(row['名称']) - if 'ST' not in stock_name and '*' not in stock_name: - stock_list.append({ - 'symbol': row['代码'], - 'name': row['名称'], - 'price': float(row['最新价']) if pd.notna(row['最新价']) else 0.0, - 'change_pct': float(row['涨跌幅']) if pd.notna(row['涨跌幅']) else 0.0, - 'volume': float(row['成交量']) if pd.notna(row['成交量']) else 0.0, - 'amount': float(row['成交额']) if pd.notna(row['成交额']) else 0.0 - }) + try: + # 过滤掉ST股票和停牌股票 + stock_name = str(row['名称']) + if 'ST' not in stock_name and '*' not in stock_name: + stock_list.append({ + 'symbol': row['代码'], + 'name': row['名称'], + 'price': float(row['最新价']) if pd.notna(row['最新价']) else 0.0, + 'change_pct': float(row['涨跌幅']) if pd.notna(row['涨跌幅']) else 0.0, + 'volume': float(row['成交量']) if pd.notna(row['成交量']) else 0.0, + 'amount': float(row['成交额']) if pd.notna(row['成交额']) else 0.0 + }) + except Exception as row_error: + print(f"处理股票数据行时出错: {row_error}") + continue # 按成交金额排序,优先显示活跃股票 stock_list.sort(key=lambda x: x['amount'], reverse=True) + print(f"成功获取 {len(stock_list)} 只股票") return stock_list + except Exception as e: print(f"获取股票列表失败: {e}") return [] diff --git a/web/templates/index.html b/web/templates/index.html index bf4a85a..589933c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -74,9 +74,51 @@ } .data-container { margin-top: 10px; + position: relative; + z-index: 10; + background-color: white; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .nav-tabs { margin-bottom: 10px; + position: relative; + z-index: 20; + background-color: white; + border-radius: 8px 8px 0 0; + padding: 10px 10px 0 10px; + } + .nav-tabs .nav-link { + border-radius: 6px 6px 0 0; + margin-right: 5px; + font-weight: 500; + transition: all 0.2s ease; + } + .nav-tabs .nav-link:hover { + background-color: #f8f9fa; + border-color: #dee2e6; + } + .nav-tabs .nav-link.active { + background-color: #0d6efd; + color: white; + border-color: #0d6efd; + } + /* 特别突出显示股票筛选tab */ + #stock-filter-tab { + background-color: #28a745 !important; + color: white !important; + border-color: #28a745 !important; + font-weight: bold !important; + box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3) !important; + } + #stock-filter-tab:hover { + background-color: #218838 !important; + border-color: #1e7e34 !important; + } + #stock-filter-tab.active { + background-color: #155724 !important; + border-color: #155724 !important; } .table-container { overflow-x: auto; @@ -478,6 +520,9 @@
筛选最近2个K线合并(KLC)中有一个满足分型强度≥1.0且分型类型不为UNKNOWN的A股股票
+| 股票代码 | +股票名称 | +分型时间 | +分型类型 | +分型强度 | +分型价格 | +当前价格 | +涨跌幅(%) | +操作 | +
|---|
错误信息:${errorMessage}
+建议:${suggestion}
++ + + 如果问题持续存在,请检查网络连接或联系管理员 + +
+ +