添加 StructureZone 结构价值区系统,支持多周期支撑/阻力分析
- 新增 ChanZone.py: 从笔中枢/线段中枢/EMA52 提取价格区,聚类评分 - ChanLun.py 新增 get_structure_zones() 方法 - web/app.py: 独立拉取多周期数据 + 缓存 + limit 传参避免全量传输 - web/index.html: 结构区勾选框 + K线数量输入 + 半透明填充区绘制 - tests/test_chan_zone.py: 24 个单元测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d8069e977f
commit
ebcb3dce73
+129
-3
@@ -12,6 +12,7 @@ import io
|
||||
import base64
|
||||
import time
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pytz import timezone
|
||||
import talib.abstract as ta
|
||||
import numpy as np
|
||||
@@ -22,6 +23,7 @@ from ChanLun import ChanLun, TF_DF
|
||||
from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_KLC_FX, Chan_FX_TYPE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR
|
||||
from cn_stock_data import ChinaStockData
|
||||
from ChanMACD import ChanMACD
|
||||
from ChanZone import StructureZoneConfig, analyze_structure_zones_from_serialized
|
||||
|
||||
# 添加买卖点枚举类型
|
||||
class TRADE_POINT_TYPE:
|
||||
@@ -48,9 +50,22 @@ china_stock = ChinaStockData()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://127.0.0.1:9009"))
|
||||
#DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://192.168.1.9:9009"))
|
||||
#DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://103.179.242.166"))
|
||||
# 结构价值区缓存: {tf_name: {'data': ..., 'expires': timestamp}}
|
||||
_zone_cache = {}
|
||||
|
||||
def _zone_cache_ttl(tf_name: str) -> int:
|
||||
"""根据时间周期返回缓存过期时间(秒)"""
|
||||
minutes = timeframe_to_minutes(tf_name) or 5
|
||||
if minutes <= 5:
|
||||
return 120 # 5m及以下: 2分钟
|
||||
elif minutes <= 15:
|
||||
return 300 # 15m: 5分钟
|
||||
elif minutes <= 60:
|
||||
return 600 # 1h: 10分钟
|
||||
else:
|
||||
return 1800 # 4h+: 30分钟
|
||||
|
||||
DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://103.179.242.166"))
|
||||
|
||||
DEFAULT_TIMEFRAME_LABELS = OrderedDict([
|
||||
("1m", "1分钟"),
|
||||
@@ -185,6 +200,8 @@ def _fetch_kl_from_datasvc(symbol, timeframe, start_ms=None, end_ms=None, limit=
|
||||
params["start"] = int(start_ms)
|
||||
if end_ms is not None:
|
||||
params["end"] = int(end_ms)
|
||||
if limit is not None:
|
||||
params["limit"] = limit
|
||||
resp = requests.get(f"{DATA_SERVICE_URL}/api/candles", params=params, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
@@ -1778,6 +1795,115 @@ def analyze():
|
||||
|
||||
pass
|
||||
|
||||
# 结构价值区分析(Structure Zone)—— 独立拉取多周期数据,缓存避免重复请求
|
||||
zone_timeframes_str = request.args.get('zone_timeframes', '')
|
||||
zone_kl_lines = int(request.args.get('zone_kl_lines', 1000))
|
||||
try:
|
||||
zone_config = StructureZoneConfig(kl_lines_per_tf=zone_kl_lines)
|
||||
if zone_timeframes_str:
|
||||
zone_config.zone_timeframes = [t.strip() for t in zone_timeframes_str.split(',') if t.strip()]
|
||||
analyses = {}
|
||||
ema52_dict = {}
|
||||
latest_close = 0.0
|
||||
now = time.time()
|
||||
|
||||
def _fetch_single_tf_zone(tf_name):
|
||||
"""单个时间周期的结构区数据拉取(线程安全)"""
|
||||
cache_key = f"{symbol}:{tf_name}:{zone_kl_lines}"
|
||||
cached = _zone_cache.get(cache_key)
|
||||
if cached and cached['expires'] > now:
|
||||
print(f" 结构区缓存命中: {tf_name}")
|
||||
return {
|
||||
'tf_name': tf_name,
|
||||
'analyses': cached['analyses'],
|
||||
'ema52': cached['ema52'],
|
||||
'close': cached.get('close', 0.0),
|
||||
'cached': True,
|
||||
}
|
||||
|
||||
try:
|
||||
tf_df = get_kl_data(symbol, tf_name, limit=zone_kl_lines)
|
||||
if tf_df is None or len(tf_df) == 0:
|
||||
return None
|
||||
tf_df = add_indicators(tf_df)
|
||||
tf_analysis = analyze_chan(tf_df, symbol, tf_name)
|
||||
zs_serialized = [{
|
||||
'start_time': (zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if zs.start_klc else None,
|
||||
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
|
||||
'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd,
|
||||
'is_sure': zs.is_sure
|
||||
} for zs in tf_analysis.get('zs_list', []) if zs.is_sure]
|
||||
bi_zs_serialized = [{
|
||||
'start_time': ((zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())),
|
||||
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
|
||||
'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd,
|
||||
'is_sure': bool(getattr(zs, 'is_sure', False))
|
||||
} for zs in tf_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)]
|
||||
last_ema = tf_df['ema52'].iloc[-1] if 'ema52' in tf_df.columns else 0
|
||||
ema_val = float(last_ema) if last_ema and last_ema > 0 else None
|
||||
last_close = float(tf_df['close'].iloc[-1])
|
||||
tf_result = {
|
||||
'tf_name': tf_name,
|
||||
'analyses': {'zs_list': zs_serialized, 'bi_zs_list': bi_zs_serialized},
|
||||
'ema52': ema_val,
|
||||
'close': last_close,
|
||||
'cached': False,
|
||||
}
|
||||
# 写入缓存
|
||||
_zone_cache[cache_key] = {
|
||||
'analyses': tf_result['analyses'],
|
||||
'ema52': ema_val,
|
||||
'close': last_close,
|
||||
'expires': now + _zone_cache_ttl(tf_name),
|
||||
}
|
||||
print(f" 结构区数据: {tf_name} -> zs={len(zs_serialized)}, bi_zs={len(bi_zs_serialized)}, ema52={ema_val}")
|
||||
return tf_result
|
||||
except Exception as e:
|
||||
print(f" 结构区 {tf_name} 拉取失败: {e}")
|
||||
return None
|
||||
|
||||
with ThreadPoolExecutor(max_workers=len(zone_config.zone_timeframes)) as executor:
|
||||
futures = {executor.submit(_fetch_single_tf_zone, tf): tf for tf in zone_config.zone_timeframes}
|
||||
for future in as_completed(futures):
|
||||
tf_result = future.result()
|
||||
if tf_result is None:
|
||||
continue
|
||||
tf_name = tf_result['tf_name']
|
||||
analyses[tf_name] = tf_result['analyses']
|
||||
ema52_dict[tf_name] = tf_result['ema52']
|
||||
if tf_result['close'] and (not latest_close or latest_close == 0.0):
|
||||
latest_close = tf_result['close']
|
||||
|
||||
structure_zones = analyze_structure_zones_from_serialized(
|
||||
analyses, ema52_dict, latest_close, config=zone_config
|
||||
)
|
||||
result['structure_zones'] = [{
|
||||
'id': z.id,
|
||||
'lower': z.lower,
|
||||
'upper': z.upper,
|
||||
'center': z.center,
|
||||
'width_pct': z.width_pct,
|
||||
'zone_type': z.zone_type,
|
||||
'timeframes': z.timeframes,
|
||||
'structure_types': z.structure_types,
|
||||
'boundary_types': z.boundary_types,
|
||||
'overlap_count': z.overlap_count,
|
||||
'touch_count': z.touch_count,
|
||||
'recency_score': z.recency_score,
|
||||
'ema52_distance_pct': z.ema52_distance_pct,
|
||||
'ema52_aligned': z.ema52_aligned,
|
||||
'strength_score': z.strength_score,
|
||||
'confidence': z.confidence,
|
||||
'first_seen': z.first_seen,
|
||||
'last_seen': z.last_seen,
|
||||
'metadata': z.metadata,
|
||||
} for z in structure_zones]
|
||||
except Exception as e:
|
||||
print(f"StructureZone 分析出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
result['structure_zones'] = []
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/symbols')
|
||||
|
||||
Reference in New Issue
Block a user