From 90cccbf1dfc0fa13f5415d99852ebffb57635b83 Mon Sep 17 00:00:00 2001 From: Lxy Date: Wed, 17 Jun 2026 21:45:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=8D=E7=9B=98?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=B0=8F=E6=95=B0=E7=82=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- app/services/plan_generator.py | 124 +++++++++++++++++++++++++++++---- app/static/futures_analysis.js | 27 ++++--- data/buffer.db | Bin 3354624 -> 3354624 bytes data/futures_analysis.db | Bin 6930432 -> 7913472 bytes docker-compose.yml | 4 +- 6 files changed, 130 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index c682cbe..d3bc198 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,6 @@ RUN mkdir -p /app/data /app/logs EXPOSE 8600 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8600/api/v1/health || exit 1 + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8600/api/v1/health')" || exit 1 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8600", "--log-level", "info"] diff --git a/app/services/plan_generator.py b/app/services/plan_generator.py index f208a18..6a87d18 100644 --- a/app/services/plan_generator.py +++ b/app/services/plan_generator.py @@ -129,14 +129,40 @@ def _calc_trend_score(t60: float, t15: float, t5: float) -> Tuple[float, str, st return round(raw, 1), direction, tag +def _get_price_precision(price: float) -> int: + """根据价格大小确定合适的小数位数""" + if price >= 10000: + return 0 # 如白银8250、股指5000+ + elif price >= 1000: + return 1 # 如原油528、黄金685(实际2位) + elif price >= 100: + return 2 # 如大部分化工品 + else: + return 2 # 小数值的品种 + + +def _round_by_price(value: float, price: float) -> float: + """根据价格精度四舍五入""" + precision = _get_price_precision(price) + return round(value, precision) + + def _calc_pivot_points(high: float, low: float, close: float) -> dict: - """计算枢轴点和支撑/阻力位""" + """计算枢轴点和支撑/阻力位(根据价格精度自动调整小数位)""" pivot = (high + low + close) / 3 r1 = 2 * pivot - low s1 = 2 * pivot - high r2 = pivot + (high - low) s2 = pivot - (high - low) - return {"pivot": pivot, "r1": r1, "r2": r2, "s1": s1, "s2": s2} + # 根据价格精度四舍五入 + p = _get_price_precision(close) + return { + "pivot": round(pivot, p), + "r1": round(r1, p), + "r2": round(r2, p), + "s1": round(s1, p), + "s2": round(s2, p), + } def _normalize_scores(values: List[float], reverse: bool = False) -> List[float]: @@ -163,6 +189,63 @@ def _normalize_abs_scores(values: List[float]) -> List[float]: return _normalize_scores(abs_values) +def _generate_trigger(score_data: dict, direction: str) -> str: + """ + 根据分析结果动态生成触发条件 + + Args: + score_data: 品种评分数据 + direction: 方向 (long/short) + + Returns: + 触发条件文本 + """ + conditions = [] + direction_tag = score_data.get("direction_tag", "震荡") + volume_ratio = score_data.get("volume_ratio", 1.0) + amplitude_pct = score_data.get("amplitude_pct", 0) + change_pct = score_data.get("change_pct", 0) + + # 根据方向标签生成基础条件 + if direction == "long": + if direction_tag == "强多": + conditions.append("价格站稳支撑位上方") + elif direction_tag == "偏多": + conditions.append("回踩支撑企稳") + else: + conditions.append("突破枢轴点P位") + else: + if direction_tag == "强空": + conditions.append("价格跌破阻力位下方") + elif direction_tag == "偏空": + conditions.append("反弹阻力受阻") + else: + conditions.append("跌破枢轴点P位") + + # 根据量比添加量能条件 + if volume_ratio > 1.5: + conditions.append("量能持续放大") + elif volume_ratio > 1.2: + conditions.append("5分钟K线放量确认") + else: + conditions.append("等待量能配合") + + # 根据振幅添加波动条件 + if amplitude_pct > 2: + conditions.append("注意波动风险") + elif amplitude_pct > 1: + conditions.append("关注区间突破") + + # 根据涨跌幅添加趋势条件 + if abs(change_pct) > 2: + if change_pct > 0: + conditions.append("强势延续确认") + else: + conditions.append("弱势延续确认") + + return " + ".join(conditions[:3]) # 最多返回3个条件 + + def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict: """ 生成V2复盘与交易计划 @@ -350,21 +433,34 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict: green_items = [s for s in raw_scores if s["category"] == "green"] for s in green_items: - direction = "long" if s["trend_score"] >= 50 else "short" + # 根据方向标签判断实际方向(强多/偏多→做多,强空/偏空→做空) + direction_tag = s.get("direction_tag", "震荡") + if direction_tag in ["强空", "偏空"]: + direction = "short" + elif direction_tag in ["强多", "偏多"]: + direction = "long" + else: + # 震荡情况下根据涨跌幅判断 + direction = "long" if s["change_pct"] > 0 else "short" + pivots = s["pivots"] + price = s["close_price"] if direction == "long": - entry_low = round(pivots["s1"], 2) - entry_high = round(pivots["pivot"], 2) - stop_loss = round(pivots["s2"], 2) - target1 = round(pivots["r1"], 2) - target2 = round(pivots["r2"], 2) + entry_low = _round_by_price(pivots["s1"], price) + entry_high = _round_by_price(pivots["pivot"], price) + stop_loss = _round_by_price(pivots["s2"], price) + target1 = _round_by_price(pivots["r1"], price) + target2 = _round_by_price(pivots["r2"], price) else: - entry_low = round(pivots["pivot"], 2) - entry_high = round(pivots["r1"], 2) - stop_loss = round(pivots["r2"], 2) - target1 = round(pivots["s1"], 2) - target2 = round(pivots["s2"], 2) + entry_low = _round_by_price(pivots["pivot"], price) + entry_high = _round_by_price(pivots["r1"], price) + stop_loss = _round_by_price(pivots["r2"], price) + target1 = _round_by_price(pivots["s1"], price) + target2 = _round_by_price(pivots["s2"], price) + + # 根据分析结果动态生成触发条件 + trigger = _generate_trigger(s, direction) plans.append({ "symbol": s["symbol"], @@ -376,7 +472,7 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict: "stop_loss": stop_loss, "target1": target1, "target2": target2, - "trigger": "回踩支撑企稳 + 5m放量突破", + "trigger": trigger, "amplitude_score": s["amplitude_score"], "volume_score": s["volume_score"], "trend_score": s["trend_score"], diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index dc71045..63c03ae 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -2411,6 +2411,15 @@ function rvRenderRisk(meta) { banner.style.display = ''; } +function rvFormatPrice(value, refPrice) { + if (!value && value !== 0) return '-'; + // 根据参考价格确定小数位数 + let precision = 2; + if (refPrice >= 10000) precision = 0; + else if (refPrice >= 1000) precision = 1; + return Number(value).toFixed(precision); +} + function rvScoreBarColor(score) { if (score > 70) return '#34C759'; // --color-down if (score > 40) return '#007AFF'; // --color-brand @@ -2473,10 +2482,10 @@ function rvRenderGreenSection(plans, scores) { ${rvScoreBar(p.activity_score || 0, '活跃')}
-
入场区间${p.entry_low || '-'} ~ ${p.entry_high || '-'}
-
止损位${p.stop_loss || '-'}
-
目标一${p.target1 || '-'}
-
目标二${p.target2 || '-'}
+
入场区间${rvFormatPrice(p.entry_low, score.close_price)} ~ ${rvFormatPrice(p.entry_high, score.close_price)}
+
止损位${rvFormatPrice(p.stop_loss, score.close_price)}
+
目标一${rvFormatPrice(p.target1, score.close_price)}
+
目标二${rvFormatPrice(p.target2, score.close_price)}
触发条件: ${p.trigger || '-'}
@@ -2614,11 +2623,11 @@ function rvRenderDetails(scores) {
综合评分${s.composite_score}
- R2: ${s.r2 || '-'} - R1: ${s.r1 || '-'} - P: ${s.pivot || '-'} - S1: ${s.s1 || '-'} - S2: ${s.s2 || '-'} + R2: ${rvFormatPrice(s.r2, s.close_price)} + R1: ${rvFormatPrice(s.r1, s.close_price)} + P: ${rvFormatPrice(s.pivot, s.close_price)} + S1: ${rvFormatPrice(s.s1, s.close_price)} + S2: ${rvFormatPrice(s.s2, s.close_price)}
`).join(''); diff --git a/data/buffer.db b/data/buffer.db index 54c9ba6136c2db28cc5878d64044ce08a925eef5..6f5b5dcb8338fbacb00e1e6649d96c714354b4c8 100644 GIT binary patch literal 3354624 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