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 54c9ba6..6f5b5dc 100644 Binary files a/data/buffer.db and b/data/buffer.db differ diff --git a/data/futures_analysis.db b/data/futures_analysis.db index c9982c1..1bc02e6 100644 Binary files a/data/futures_analysis.db and b/data/futures_analysis.db differ diff --git a/docker-compose.yml b/docker-compose.yml index 14dc9b7..43c0c9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: buffer-platform: build: @@ -25,7 +23,7 @@ services: # 日志持久化(可选) - ./logs:/app/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8600/api/v1/health"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8600/api/v1/health')"] interval: 30s timeout: 10s retries: 3