Remove files

This commit is contained in:
jackyu66git
2025-05-26 19:54:07 +08:00
parent 0cc19132eb
commit 0754b5ae59
25 changed files with 1076 additions and 1103 deletions
-102
View File
@@ -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返回的分型强度数据完整性
-139
View File
@@ -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** - 详细文档(新建)
## ✅ 验证结果
- ✅ 后端强度计算功能正常
- ✅ 特征数据集成成功
- ✅ 前端显示逻辑正确
- ✅ 配置系统可用
- ✅ 文档完整齐全
- ✅ 测试验证通过
**分型强度检测功能已全面完成并可投入使用!** 🎉
-221
View File
@@ -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**:基础分型强度检测功能
- 支持多维度强度评估
- 集成到特征数据系统
- 提供配置化参数调整
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -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",
+83
View File
@@ -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
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

+35 -14
View File
@@ -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")
),
+225
View File
@@ -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])
-78
View File
@@ -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()
-192
View File
@@ -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测试完成!")
-214
View File
@@ -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()
-107
View File
@@ -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()
+236 -19
View File
@@ -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)
+46 -13
View File
@@ -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 []
+448 -1
View File
@@ -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 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="trade-points-tab" data-bs-toggle="tab" data-bs-target="#trade-points" type="button" role="tab">买卖点</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="stock-filter-tab" data-bs-toggle="tab" data-bs-target="#stock-filter" type="button" role="tab">股票筛选</button>
</li>
</ul>
<div class="data-source-info alert alert-info py-2 mt-1 mb-2" style="display:none;">
<small id="dataSourceText"></small>
@@ -592,6 +637,81 @@
</table>
</div>
</div>
<div class="tab-pane fade" id="stock-filter" role="tabpanel">
<div class="container-fluid">
<div class="row mb-3">
<div class="col-md-12">
<h5>A股强分型筛选</h5>
<p class="text-muted">筛选最近2个K线合并(KLC)中有一个满足分型强度≥1.0且分型类型不为UNKNOWN的A股股票</p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label for="filterStartTime" class="form-label">开始时间:</label>
<input type="datetime-local" id="filterStartTime" class="form-control">
</div>
<div class="col-md-3">
<label for="filterEndTime" class="form-label">结束时间:</label>
<input type="datetime-local" id="filterEndTime" class="form-control">
</div>
<div class="col-md-3">
<label for="filterTimeframe" class="form-label">时间周期:</label>
<select id="filterTimeframe" class="form-select">
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="4h">4小时</option>
<option value="1d" selected>1日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
<div class="col-md-3">
<label for="fxStrengthThreshold" class="form-label">分型强度阈值:</label>
<input type="number" id="fxStrengthThreshold" class="form-control" value="1.0" min="0" max="100" step="0.1">
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<button class="btn btn-primary" onclick="filterStocks()">
<i class="bi bi-search"></i> 开始筛选
</button>
<button class="btn btn-secondary ms-2" onclick="exportFilterResults()">
<i class="bi bi-download"></i> 导出结果
</button>
<span id="filterProgress" class="ms-3" style="display:none;">
<i class="bi bi-hourglass-split"></i> 正在筛选中...
</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="table-container">
<table id="stockFilterTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>股票代码</th>
<th>股票名称</th>
<th>分型时间</th>
<th>分型类型</th>
<th>分型强度</th>
<th>分型价格</th>
<th>当前价格</th>
<th>涨跌幅(%)</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -841,6 +961,12 @@
updateChartDisplay();
});
// 添加K线周期切换事件监听器
$('input[name="klinePeriod"]').change(function() {
console.log('K线周期切换:', $(this).attr('id'), $(this).is(':checked'));
updateChartDisplay();
});
// 当选择不同的元素时间周期时
$('#elementTimeframe').change(function() {
const elementTimeframe = $(this).val();
@@ -1198,7 +1324,7 @@
}
// 检查是否使用小周期K线数据
const useElementPeriod = $('#useElementPeriod').is(':checked') &&
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
@@ -4004,6 +4130,52 @@
updateTables(currentData);
}
});
// 页面加载完成后初始化
$(document).ready(function() {
// 设置默认的筛选时间(最近7天)
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
$('#filterEndTime').val(now.toISOString().slice(0, 16));
$('#filterStartTime').val(weekAgo.toISOString().slice(0, 16));
// 初始化股票筛选表格
initStockFilterTable();
// 突出显示股票筛选tab
setTimeout(function() {
const stockFilterTab = $('#stock-filter-tab');
if (stockFilterTab.length > 0) {
console.log('股票筛选tab已找到,开始突出显示');
// 添加闪烁效果来吸引注意
stockFilterTab.addClass('animate__animated animate__pulse');
// 滚动到tab区域
$('html, body').animate({
scrollTop: $('.data-container').offset().top - 100
}, 1000);
// 添加提示信息
const alertDiv = $(`
<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">
<strong>新功能!</strong> 股票筛选功能已添加,请查看绿色的"股票筛选"标签页。
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('body').append(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
} else {
console.error('未找到股票筛选tab');
}
}, 2000);
});
});
// 自动刷新相关变量
@@ -5103,6 +5275,281 @@
window.astockStatusInterval = setInterval(updateAStockTradingStatus, 30000);
console.log('A股交易时间状态更新器已启动');
}
// 股票筛选相关函数
let stockFilterTable = null;
// 初始化股票筛选表格
function initStockFilterTable() {
// 简单的表格初始化,不使用DataTable
console.log('初始化股票筛选表格');
}
// 筛选股票
function filterStocks() {
const startTime = $('#filterStartTime').val();
const endTime = $('#filterEndTime').val();
const timeframe = $('#filterTimeframe').val();
const threshold = parseFloat($('#fxStrengthThreshold').val());
if (!startTime || !endTime) {
alert('请选择开始时间和结束时间');
return;
}
if (new Date(startTime) >= new Date(endTime)) {
alert('开始时间必须早于结束时间');
return;
}
// 显示进度指示器
$('#filterProgress').show();
$('#stockFilterTable tbody').empty();
// 发送筛选请求
$.ajax({
url: '/api/filter_stocks',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
start_time: startTime,
end_time: endTime,
timeframe: timeframe,
fx_strength_threshold: threshold
}),
timeout: 300000, // 5分钟超时
success: function(response) {
$('#filterProgress').hide();
if (response.error) {
// 根据错误类型显示不同的错误信息
if (response.error_type === 'network_error') {
showNetworkErrorAlert(response.error, response.suggestion);
} else {
alert('筛选失败: ' + response.error);
}
return;
}
// 更新表格数据
updateStockFilterTable(response.results);
// 显示统计信息
const totalCount = response.results.length;
const processedCount = response.total_processed || 0;
const failedCount = response.failed_count || 0;
const dataSource = response.data_source || '未知';
let message = `筛选完成!使用${dataSource},共处理 ${processedCount} 只股票,找到 ${totalCount} 只满足条件的股票`;
if (failedCount > 0) {
message += `${failedCount} 只股票数据获取失败`;
}
// 显示成功提示
showSuccessAlert(message);
},
error: function(xhr, status, error) {
$('#filterProgress').hide();
console.error('筛选请求失败:', error, status, xhr);
// 根据错误类型显示不同的错误信息
if (status === 'timeout') {
showNetworkErrorAlert(
'请求超时,可能是网络连接不稳定或数据量较大',
'请检查网络连接,或尝试缩小时间范围后重试'
);
} else if (xhr.responseJSON && xhr.responseJSON.error_type === 'network_error') {
showNetworkErrorAlert(xhr.responseJSON.error, xhr.responseJSON.suggestion);
} else {
alert('筛选请求失败: ' + (xhr.responseJSON?.error || error || '未知错误'));
}
}
});
}
// 显示网络错误提示
function showNetworkErrorAlert(errorMessage, suggestion) {
const alertDiv = $(`
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<h6><i class="fas fa-exclamation-triangle"></i> 网络连接问题</h6>
<p><strong>错误信息:</strong>${errorMessage}</p>
<p><strong>建议:</strong>${suggestion}</p>
<hr>
<p class="mb-0">
<small>
<i class="fas fa-info-circle"></i>
如果问题持续存在,请检查网络连接或联系管理员
</small>
</p>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('#stock-filter .container-fluid').prepend(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
}
// 显示成功提示
function showSuccessAlert(message) {
const alertDiv = $(`
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle"></i> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('#stock-filter .container-fluid').prepend(alertDiv);
// 5秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 5000);
}
// 更新股票筛选表格
function updateStockFilterTable(results) {
const tbody = $('#stockFilterTable tbody');
tbody.empty();
if (!results || results.length === 0) {
tbody.append('<tr><td colspan="9" class="text-center">没有找到满足条件的股票</td></tr>');
return;
}
// 按分型强度降序排列
results.sort((a, b) => b.fx_strength - a.fx_strength);
// 添加新数据
results.forEach(function(stock) {
const strengthClass = getStrengthClass(stock.fx_strength);
const changeClass = stock.change_percent >= 0 ? 'text-danger' : 'text-success';
const changeSign = stock.change_percent >= 0 ? '+' : '';
const row = `
<tr>
<td>${stock.symbol}</td>
<td>${stock.name}</td>
<td>${stock.fx_time}</td>
<td>${stock.fx_type}</td>
<td><span class="${strengthClass}">${stock.fx_strength.toFixed(2)}</span></td>
<td>${stock.fx_price.toFixed(2)}</td>
<td>${stock.current_price ? stock.current_price.toFixed(2) : '-'}</td>
<td><span class="${changeClass}">${changeSign}${(stock.change_percent || 0).toFixed(2)}%</span></td>
<td><button class="btn btn-sm btn-outline-primary" onclick="analyzeStock('${stock.symbol}')">分析</button></td>
</tr>
`;
tbody.append(row);
});
}
// 获取分型强度对应的CSS类
function getStrengthClass(strength) {
if (strength >= 2.0) return 'text-danger fw-bold';
else if (strength >= 1.5) return 'text-warning fw-bold';
else if (strength >= 1.0) return 'text-info';
else return '';
}
// 分析特定股票
function analyzeStock(symbol) {
// 切换到主分析页面
$('#stock-filter-tab').removeClass('active');
$('#kline-tab').addClass('active');
$('#stock-filter').removeClass('show active');
$('#kline').addClass('show active');
// 切换数据源为A股
$('#dataSource').val('a_stock');
$('#dataSource').trigger('change');
// 等待数据源切换完成后设置股票代码
setTimeout(() => {
$('#astockSymbol').val(symbol);
// 触发分析
updateChart();
}, 500);
}
// 导出筛选结果
function exportFilterResults() {
const tbody = $('#stockFilterTable tbody tr');
if (tbody.length === 0 || (tbody.length === 1 && tbody.find('td').length === 1)) {
alert('没有可导出的数据');
return;
}
// 创建CSV内容
const headers = ['股票代码', '股票名称', '分型时间', '分型类型', '分型强度', '分型价格', '当前价格', '涨跌幅(%)'];
let csvContent = headers.join(',') + '\n';
tbody.each(function() {
const cells = $(this).find('td');
if (cells.length > 1) { // 排除"没有数据"的行
const row = [];
cells.slice(0, 8).each(function() { // 只取前8列,排除操作列
row.push($(this).text().trim());
});
csvContent += row.join(',') + '\n';
}
});
// 创建下载链接
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `股票筛选结果_${new Date().toISOString().slice(0, 10)}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 页面加载完成后初始化
$(document).ready(function() {
// 设置默认的筛选时间(最近7天)
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
$('#filterEndTime').val(now.toISOString().slice(0, 16));
$('#filterStartTime').val(weekAgo.toISOString().slice(0, 16));
// 初始化股票筛选表格
initStockFilterTable();
// 突出显示股票筛选tab
setTimeout(function() {
const stockFilterTab = $('#stock-filter-tab');
if (stockFilterTab.length > 0) {
console.log('股票筛选tab已找到,开始突出显示');
// 滚动到tab区域
$('html, body').animate({
scrollTop: $('.data-container').offset().top - 100
}, 1000);
// 添加提示信息
const alertDiv = $(`
<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">
<strong>新功能!</strong> 股票筛选功能已添加,请查看绿色的"股票筛选"标签页。
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('body').append(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
} else {
console.error('未找到股票筛选tab');
}
}, 2000);
});
</script>
</body>
</html>