diff --git a/.gitignore b/.gitignore index 807b6c0..c7cd41d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ dist/ archive/ backups/ tmp/ +legacy/scratch/ +reports/*.json diff --git a/= b/= deleted file mode 100644 index e69de29..0000000 diff --git a/AGENTS.md b/AGENTS.md index 4c81e81..4e5f32e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 - 后端:`FastAPI`, `uvicorn`, `pydantic` - 数据与计算:`sqlite3`, `pandas`, `numpy` - 交易所/行情:`ccxt`, `requests` -- 配置:`rules.yaml` + `config_loader.py` +- 配置:`rules.yaml` + `app/config/config_loader.py` - 测试:`pytest` / `unittest` - 部署:`Dockerfile`, `docker-compose.yml` - 前端:`static/*.html` 模板页,由 FastAPI/Jinja2 提供页面壳和 API @@ -22,28 +22,28 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 建议把系统理解为 6 个层次: -1. `altcoin_screener.py` +1. `app/services/altcoin_screener.py` 负责粗筛,基于 Binance 行情、量价/结构等规则找候选币。 -2. `altcoin_confirm.py` +2. `app/services/altcoin_confirm.py` 负责确认,判断是否形成更可执行的机会,并生成入场计划、上下文和推送候选。 -3. `price_tracker.py` +3. `app/services/price_tracker.py` 负责跟踪活跃推荐,更新盈亏、止盈止损、趋势衰减、行动状态。 -4. `review_engine.py` +4. `app/services/review_engine.py` 负责复盘与策略自迭代,包括信号绩效、漏选复盘、规则候选、版本演进。 -5. `event_driven_screener.py` +5. `app/services/event_driven_screener.py` 负责事件/舆情驱动的快速触发检查,属于技术筛选主链路的补充入口。 -6. `web_server.py` +6. `app/web/web_server.py` 负责用户端和管理端 API、页面壳、订阅与认证相关接口。 ### 3.2 数据与状态中心 -- `altcoin_db.py` 是交易/推荐/状态的核心数据库层,体量很大,承担了: +- `app/db/altcoin_db.py` 是交易/推荐/状态的核心数据库层,体量很大,承担了: - 初始化表结构 - recommendation / screening_log / tracking / review 等主表读写 - 推荐状态派生与展示口径整理 - 部分状态迁移与兼容逻辑 -- `auth_db.py` 是会员、邀请码、邮箱验证、订阅、订单预留的数据库层。 -- `opportunity_lifecycle.py` 是机会生命周期和买点质量闸门的规则中心,决定: +- `app/db/auth_db.py` 是会员、邀请码、邮箱验证、订阅、订单预留的数据库层。 +- `app/core/opportunity_lifecycle.py` 是机会生命周期和买点质量闸门的规则中心,决定: - 哪些机会只是观察池 - 哪些机会可以进入“可即刻买入” - 哪些状态应该被视为历史/盈利管理/观察态 @@ -51,18 +51,20 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 ### 3.3 配置中心 - `rules.yaml` 是策略配置单一事实源。 -- `config_loader.py` 负责: +- `app/config/config_loader.py` 负责: - 读取/缓存配置 - 暴露各子模块配置访问函数 - 将部分复盘后的参数改写回 `rules.yaml` - 兼容旧信号名 -如果要改筛选阈值、确认门槛、止盈止损、动态权重逻辑,优先检查 `rules.yaml` 和 `config_loader.py`,不要直接在业务脚本里硬编码新参数。 +如果要改筛选阈值、确认门槛、止盈止损、动态权重逻辑,优先检查 `rules.yaml` 和 `app/config/config_loader.py`,不要直接在业务脚本里硬编码新参数。 ## 4. 目录速览 ### 4.1 核心目录 +- `/app` + - 当前真实实现层,按职责拆成 `services`, `db`, `core`, `config`, `integrations`, `analysis`, `web` - `/static` - 页面文件,如 `app.html`, `auth.html`, `subscription.html`, `strategy.html` - `/tests` @@ -71,6 +73,14 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 - 若干结构/状态机/信号时效性校验脚本 - `/docker` - 容器入口与串行调度器 +- `/tools` + - 非主链路工具脚本,如回测和输出摘要脚本 +- `/templates` + - 被后端读取的 HTML 模板资源 +- `/reports` + - 本地分析/回测结果产物 +- `/legacy` + - 历史实验脚本、旧页面备份、临时整理归档 - `/data` - SQLite 数据库存放目录 - `/logs` @@ -80,23 +90,36 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 ### 4.2 根目录关键文件 -- [web_server.py](/Users/aaron/Desktop/code/alphax-docker/web_server.py) -- [altcoin_db.py](/Users/aaron/Desktop/code/alphax-docker/altcoin_db.py) -- [auth_db.py](/Users/aaron/Desktop/code/alphax-docker/auth_db.py) -- [altcoin_screener.py](/Users/aaron/Desktop/code/alphax-docker/altcoin_screener.py) -- [altcoin_confirm.py](/Users/aaron/Desktop/code/alphax-docker/altcoin_confirm.py) -- [price_tracker.py](/Users/aaron/Desktop/code/alphax-docker/price_tracker.py) -- [review_engine.py](/Users/aaron/Desktop/code/alphax-docker/review_engine.py) -- [event_driven_screener.py](/Users/aaron/Desktop/code/alphax-docker/event_driven_screener.py) -- [opportunity_lifecycle.py](/Users/aaron/Desktop/code/alphax-docker/opportunity_lifecycle.py) - [rules.yaml](/Users/aaron/Desktop/code/alphax-docker/rules.yaml) -- [config_loader.py](/Users/aaron/Desktop/code/alphax-docker/config_loader.py) - [docker-compose.yml](/Users/aaron/Desktop/code/alphax-docker/docker-compose.yml) - [README_DOCKER.md](/Users/aaron/Desktop/code/alphax-docker/README_DOCKER.md) +### 4.3 根目录保留原则 + +根目录应尽量只保留这些类型: + +- 顶层配置 +- Docker 入口与部署文件 +- 项目说明文档 +- 明确约定的非代码资产目录 + +Python 业务实现不应再直接留在根目录。 + +像下面这些内容应放在分层目录里: + +- 服务流程:`/app/services` +- 数据访问:`/app/db` +- 领域与规则:`/app/core` +- 配置访问:`/app/config` +- Web 层:`/app/web` +- 第三方集成:`/app/integrations` +- 顶层配置 +- Docker 入口 +- 关键说明文档 + ## 5. Web/API 观察 -`web_server.py` 体量非常大,既包含: +`app/web/web_server.py` 体量非常大,既包含: - 认证接口 - 订阅接口 @@ -107,11 +130,11 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 如果要改 Web 逻辑,先确认变更属于哪一类: -- “数据口径问题”优先去 `altcoin_db.py` / `opportunity_lifecycle.py` +- “数据口径问题”优先去 `app/db/altcoin_db.py` / `app/core/opportunity_lifecycle.py` - “参数问题”优先去 `rules.yaml` - “页面展示问题”再回到 `static/*.html` -不要第一反应直接在 `web_server.py` 里堆业务分支,否则会继续放大这个文件的复杂度。 +不要第一反应直接在 `app/web/web_server.py` 里堆业务分支,否则会继续放大这个文件的复杂度。 ## 6. 调度与运行方式 @@ -135,16 +158,16 @@ AlphaX 是一个以 `Python + FastAPI + SQLite + 静态 HTML` 组成的加密市 ### 6.2 入口 - `docker/entrypoint.sh` - - `web` -> 启动 uvicorn + - `web` -> 启动 uvicorn (`app.web.web_server:app`) - `scheduler` -> 启动 `docker/scheduler.py` - `once` -> 执行单次脚本 - `docker/scheduler.py` - - 统一调度 `event_driven_screener.py` - - `price_tracker.py` - - `altcoin_confirm.py` - - `altcoin_screener.py` - - `sentiment_monitor.py` - - `review_engine.py` + - 统一调度 `app.services.event_driven_screener` + - `app.services.price_tracker` + - `app.services.altcoin_confirm` + - `app.services.altcoin_screener` + - `app.services.sentiment_monitor` + - `app.services.review_engine` ## 7. 测试与验证建议 @@ -184,10 +207,10 @@ python3 scripts/validate_signal_recency.py ### 8.1 改动前先判断“应该改哪一层” - 调参数:优先 `rules.yaml` -- 配置读取/兼容:`config_loader.py` -- 状态口径:`opportunity_lifecycle.py` 或 `altcoin_db.py` -- DB 表结构/查询:`altcoin_db.py` / `auth_db.py` -- API 契约:`web_server.py` +- 配置读取/兼容:`app/config/config_loader.py` +- 状态口径:`app/core/opportunity_lifecycle.py` 或 `app/db/altcoin_db.py` +- DB 表结构/查询:`app/db/altcoin_db.py` / `app/db/auth_db.py` +- API 契约:`app/web/web_server.py` - 页面壳和交互:`static/*.html` ### 8.2 SQLite 相关约束 @@ -220,8 +243,8 @@ python3 scripts/validate_signal_recency.py 这个仓库里有不少“迭代中兼容旧逻辑”的痕迹,例如: -- `config_loader.py` 中的信号别名兼容 -- `altcoin_db.py` 中大量 `ALTER TABLE` 迁移兜底 +- `app/config/config_loader.py` 中的信号别名兼容 +- `app/db/altcoin_db.py` 中大量 `ALTER TABLE` 迁移兜底 - 多处 `entry_plan_json` / `detail_json` / `*_context_json` 因此改动时应优先做增量兼容,而不是假设数据库、配置、旧数据永远是干净新鲜的。 @@ -231,14 +254,14 @@ python3 scripts/validate_signal_recency.py - 当前目录 **不是 git 仓库根目录**,`git status` 会失败;如果后续需要版本管理,请先确认真正的 Git 根目录或重新初始化。 - [DESIGN.md](/Users/aaron/Desktop/code/alphax-docker/DESIGN.md) 当前内容更像一份品牌/样式 YAML,不是这个项目的系统设计文档,阅读时不要误判。 - 根目录存在一些临时/非核心文件,例如 `.tmp_patch_tp1.py`、`.tmp_strategy_v2_marker.txt`、`=`,后续开发前建议先确认这些文件是否仍有保留价值。 -- `web_server.py` 和 `altcoin_db.py` 都已经非常大,后续新增功能应尽量避免继续把复杂度集中到这两个文件。 +- `app/web/web_server.py` 和 `app/db/altcoin_db.py` 都已经非常大,后续新增功能应尽量避免继续把复杂度集中到这两个文件。 ## 10. 推荐的后续重构方向 后续若继续开发,建议优先考虑这几个方向: -1. 把 `web_server.py` 按认证、推荐、策略、管理端拆分路由模块。 -2. 把 `altcoin_db.py` 拆成 schema/init、recommendation、review、analytics、admin 查询几个子模块。 +1. 把 `app/web/web_server.py` 按认证、推荐、策略、管理端拆分路由模块。 +2. 把 `app/db/altcoin_db.py` 拆成 schema/init、recommendation、review、analytics、admin 查询几个子模块。 3. 为 `rules.yaml` 建立更明确的 schema 校验,避免配置漂移。 4. 给核心脚本增加更稳定的 CLI 参数入口,而不是依赖脚本内默认行为。 5. 梳理推送链路,把“是否推送”的判断和“推送内容生成”进一步解耦。 diff --git a/README_DOCKER.md b/README_DOCKER.md index cdeaf48..6ba5e51 100644 --- a/README_DOCKER.md +++ b/README_DOCKER.md @@ -53,18 +53,18 @@ docker compose up -d alphax-scheduler | 任务 | 脚本 | 间隔 | |---|---|---| -| 事件舆情 | `event_driven_screener.py --once` | 60s | -| 价格跟踪 | `price_tracker.py` | 180s | -| 爆发确认 | `altcoin_confirm.py` | 600s | -| 粗筛/细筛 | `altcoin_screener.py` | 900s | -| 舆情采集 | `sentiment_monitor.py --collect` | 1800s | -| 复盘 | `review_engine.py` | 24h | +| 事件舆情 | `python -m app.services.event_driven_screener --once` | 60s | +| 价格跟踪 | `python -m app.services.price_tracker` | 180s | +| 爆发确认 | `python -m app.services.altcoin_confirm` | 600s | +| 粗筛/细筛 | `python -m app.services.altcoin_screener` | 900s | +| 舆情采集 | `python -m app.services.sentiment_monitor --collect` | 1800s | +| 复盘 | `python -m app.services.review_engine` | 24h | ## 验证命令 ```bash cd /home/ubuntu/quant_monitor/alphax-docker -python3 -m py_compile altcoin_db.py auth_db.py opportunity_lifecycle.py altcoin_screener.py altcoin_confirm.py price_tracker.py event_driven_screener.py sentiment_monitor.py review_engine.py web_server.py docker/scheduler.py scripts/validate_docker_layout.py +python3 -m py_compile app/db/altcoin_db.py app/db/auth_db.py app/core/opportunity_lifecycle.py app/services/altcoin_screener.py app/services/altcoin_confirm.py app/services/price_tracker.py app/services/event_driven_screener.py app/services/sentiment_monitor.py app/services/review_engine.py app/web/web_server.py docker/scheduler.py scripts/validate_docker_layout.py python3 scripts/validate_docker_layout.py python3 scripts/validate_state_machine.py python3 scripts/validate_push_state_flow.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..a48d373 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""Application package for AlphaX.""" diff --git a/app/analysis/__init__.py b/app/analysis/__init__.py new file mode 100644 index 0000000..8145bad --- /dev/null +++ b/app/analysis/__init__.py @@ -0,0 +1 @@ +"""Analysis modules.""" diff --git a/reverse_analysis.py b/app/analysis/reverse_analysis.py similarity index 98% rename from reverse_analysis.py rename to app/analysis/reverse_analysis.py index 15a9ba8..5ed17b4 100644 --- a/reverse_analysis.py +++ b/app/analysis/reverse_analysis.py @@ -21,10 +21,10 @@ import requests import pandas as pd sys.path.insert(0, os.path.dirname(__file__)) -from altcoin_db import get_conn, record_missed_explosion, upsert_strategy_rule_candidate -from pa_engine import full_pa_analysis, classify_candles, calc_atr -from sector_map import get_sector_for_coin, COIN_TO_SECTORS, SECTOR_MEMBERS -import config_loader +from app.db.altcoin_db import get_conn, record_missed_explosion, upsert_strategy_rule_candidate +from app.core.pa_engine import full_pa_analysis, classify_candles, calc_atr +from app.core.sector_map import get_sector_for_coin, COIN_TO_SECTORS, SECTOR_MEMBERS +from app.config import config_loader BINANCE_API = "https://api.binance.com/api/v3" @@ -576,4 +576,4 @@ def run_reverse_analysis(): if __name__ == "__main__": results = run_reverse_analysis() - print(json.dumps(results, ensure_ascii=False, indent=2)) \ No newline at end of file + print(json.dumps(results, ensure_ascii=False, indent=2)) diff --git a/app/config/__init__.py b/app/config/__init__.py new file mode 100644 index 0000000..3d6508b --- /dev/null +++ b/app/config/__init__.py @@ -0,0 +1 @@ +"""Configuration modules.""" diff --git a/config_loader.py b/app/config/config_loader.py similarity index 98% rename from config_loader.py rename to app/config/config_loader.py index a9614f8..7928fb0 100644 --- a/config_loader.py +++ b/app/config/config_loader.py @@ -5,11 +5,13 @@ review_engine 调整权重后直接写回 yaml,下次运行自动生效 import copy import datetime import os +from pathlib import Path import yaml -RULES_PATH = os.path.join(os.path.dirname(__file__), "rules.yaml") +REPO_ROOT = Path(__file__).resolve().parents[2] +RULES_PATH = str(REPO_ROOT / "rules.yaml") _cache = None _cache_mtime = None @@ -154,7 +156,7 @@ def get_signal_weights(): canonical[normalize_signal_name(sig)] = weight try: - from altcoin_db import get_signal_weights as db_get_weights + from app.db.altcoin_db import get_signal_weights as db_get_weights db_weights = db_get_weights() for sig, data in db_weights.items(): norm_sig = normalize_signal_name(sig) @@ -226,7 +228,7 @@ def update_signal_weight(signal_name, new_weight): rules.setdefault("signal_weights", {})[canonical_name] = new_weight save_rules(rules) try: - from altcoin_db import update_signal_performance + from app.db.altcoin_db import update_signal_performance update_signal_performance(canonical_name, category=None, is_hit=None, pnl=None, weight_override=new_weight) except Exception: pass diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..7d75799 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +"""Core strategy and domain modules.""" diff --git a/opportunity_lifecycle.py b/app/core/opportunity_lifecycle.py similarity index 100% rename from opportunity_lifecycle.py rename to app/core/opportunity_lifecycle.py diff --git a/pa_engine.py b/app/core/pa_engine.py similarity index 99% rename from pa_engine.py rename to app/core/pa_engine.py index 608bedc..06c924b 100644 --- a/pa_engine.py +++ b/app/core/pa_engine.py @@ -13,7 +13,7 @@ import pandas as pd import numpy as np from typing import List, Dict, Optional, Tuple -from config_loader import ( +from app.config.config_loader import ( dynamic_k_thresholds, static_k_thresholds, zone_params, @@ -781,4 +781,4 @@ def full_pa_analysis(df: pd.DataFrame, timeframe: str = "4h") -> Dict: "ignition_points": ignition_points, "atr": round(atr, 6), "trend_exhaustion": exhaustion, - } \ No newline at end of file + } diff --git a/sector_map.py b/app/core/sector_map.py similarity index 100% rename from sector_map.py rename to app/core/sector_map.py diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..bdd0c41 --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1 @@ +"""Database access modules.""" diff --git a/altcoin_db.py b/app/db/altcoin_db.py similarity index 99% rename from altcoin_db.py rename to app/db/altcoin_db.py index ac28438..fc0af00 100644 --- a/altcoin_db.py +++ b/app/db/altcoin_db.py @@ -8,9 +8,10 @@ import json import os import re from datetime import datetime, timedelta +from pathlib import Path -from config_loader import get_meta, get_screener_section, confirm_state_cooldown_hours -from opportunity_lifecycle import ( +from app.config.config_loader import get_meta, get_screener_section, confirm_state_cooldown_hours +from app.core.opportunity_lifecycle import ( apply_entry_quality_gate, normalize_json_object, derive_display_bucket, @@ -18,7 +19,8 @@ from opportunity_lifecycle import ( is_executed_lifecycle, ) -DB_PATH = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) +REPO_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) def get_conn(): @@ -2177,7 +2179,7 @@ def get_review_stats(): conn = get_conn() revision_started_at = "" try: - from config_loader import get_meta + from app.config.config_loader import get_meta meta = get_meta() or {} revision_started_at = (meta.get("strategy_revision_started_at") or "").strip() except Exception: diff --git a/auth_db.py b/app/db/auth_db.py similarity index 99% rename from auth_db.py rename to app/db/auth_db.py index 4a8406e..a460046 100644 --- a/auth_db.py +++ b/app/db/auth_db.py @@ -18,9 +18,11 @@ import smtplib import sqlite3 from datetime import datetime, timedelta from email.message import EmailMessage +from pathlib import Path from typing import Optional -DB_PATH = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) +REPO_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) SESSION_DAYS = 30 VERIFY_CODE_MINUTES = 15 FREE_TRIAL_DAYS = 30 diff --git a/app/integrations/__init__.py b/app/integrations/__init__.py new file mode 100644 index 0000000..13600af --- /dev/null +++ b/app/integrations/__init__.py @@ -0,0 +1 @@ +"""External integration modules.""" diff --git a/feishu_push.py b/app/integrations/feishu_push.py similarity index 100% rename from feishu_push.py rename to app/integrations/feishu_push.py diff --git a/feishu_review_push.py b/app/integrations/feishu_review_push.py similarity index 98% rename from feishu_review_push.py rename to app/integrations/feishu_review_push.py index 2a4c51c..b5d3a9b 100644 --- a/feishu_review_push.py +++ b/app/integrations/feishu_review_push.py @@ -14,7 +14,7 @@ import sys import json sys.path.insert(0, os.path.dirname(__file__)) -from feishu_push import push_card +from app.integrations.feishu_push import push_card CHAT_ID = "oc_2c597ad94167102922de142928e2917a" @@ -53,7 +53,7 @@ def push_review_report(review_results): # Section 2: 信号绩效TOP5 # 从review_results中提取信号绩效信息 - from altcoin_db import get_signal_weights + from app.db.altcoin_db import get_signal_weights weights = get_signal_weights() sig_perf_list = sorted( [(sig, data) for sig, data in weights.items() if data.get("total_count", 0) >= 3], @@ -295,4 +295,4 @@ if __name__ == "__main__": # 测试rule notification ok2, r2 = push_rule_update_notification("rule_20260429_001", "涨幅榜60%有起爆点 → 起爆点是爆发前必现信号") - print(f"规律通知: ok={ok2}") \ No newline at end of file + print(f"规律通知: ok={ok2}") diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..7feffea --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1 @@ +"""Service and workflow modules.""" diff --git a/altcoin_confirm.py b/app/services/altcoin_confirm.py similarity index 99% rename from altcoin_confirm.py rename to app/services/altcoin_confirm.py index 985df5a..d511ad0 100644 --- a/altcoin_confirm.py +++ b/app/services/altcoin_confirm.py @@ -22,16 +22,17 @@ import os import time import requests from datetime import datetime, timedelta +from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) -from sector_map import get_burst_threshold, is_meme_coin, get_sector_for_coin, COIN_TO_SECTORS -from altcoin_db import ( +from app.core.sector_map import get_burst_threshold, is_meme_coin, get_sector_for_coin, COIN_TO_SECTORS +from app.db.altcoin_db import ( init_db, expire_old_states, expire_old_recommendations, get_candidates_for_confirm, update_state, get_conn, create_recommendation, log_screening, log_cron_run, should_push, log_push, update_latest_price_cache, get_recommendation_for_push, ) -from feishu_push import push_recommendation_state_alert -from config_loader import ( +from app.integrations.feishu_push import push_recommendation_state_alert +from app.config.config_loader import ( get_strategy_direction, vp_fly_params, confirm_min_score, @@ -40,15 +41,16 @@ from config_loader import ( confirm_stop_loss_params, get_strategy_params, ) -from opportunity_lifecycle import apply_entry_quality_gate -from config_loader import _get_section as _get_cfg_section -from pa_engine import ( +from app.core.opportunity_lifecycle import apply_entry_quality_gate +from app.config.config_loader import _get_section as _get_cfg_section +from app.core.pa_engine import ( classify_candles, calc_atr, find_supply_demand_zones, find_continuous_k, detect_ignition_point, full_pa_analysis, analyze_entry_point, detect_trend_exhaustion, ) exchange = ccxt.binance({"enableRateLimit": True}) +REPO_ROOT = Path(__file__).resolve().parents[2] def fetch_klines(symbol, timeframe, limit=200): @@ -66,7 +68,7 @@ def symbol_recently_closed(symbol: str, hours: int = 8) -> bool: 用于冷却期:刚止盈的币不宜立即追入。""" import sqlite3, os from datetime import datetime, timezone, timedelta - db = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) + db = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) conn = sqlite3.connect(db) cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat() row = conn.execute(""" @@ -1340,4 +1342,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/altcoin_screener.py b/app/services/altcoin_screener.py similarity index 99% rename from altcoin_screener.py rename to app/services/altcoin_screener.py index 111463b..81a0fe9 100644 --- a/altcoin_screener.py +++ b/app/services/altcoin_screener.py @@ -23,20 +23,21 @@ import os import time import requests from datetime import datetime +from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) -from sector_map import ( +from app.core.sector_map import ( SECTOR_MEMBERS, COIN_TO_SECTORS, MEME_SECTORS, MIN_24H_VOLUME_USD, MEME_MIN_24H_VOLUME_USD, get_sector_for_coin, is_meme_coin, get_burst_threshold, dynamic_leader_detection, ) -from altcoin_db import ( +from app.db.altcoin_db import ( init_db, expire_old_states, update_state, get_candidates_for_confirm, log_screening, create_recommendation, expire_old_recommendations, log_cron_run, ) -from config_loader import ( +from app.config.config_loader import ( get_signal_weights, get_strategy_direction, get_meta, @@ -48,12 +49,13 @@ from config_loader import ( get_screener_section, sentiment_max_bonus, ) -from pa_engine import ( +from app.core.pa_engine import ( classify_candles, calc_atr, find_supply_demand_zones, find_continuous_k, detect_ignition_point, full_pa_analysis, ) exchange = ccxt.binance({"enableRateLimit": True}) +REPO_ROOT = Path(__file__).resolve().parents[2] # ==================== 排除列表 ==================== STABLECOINS = { @@ -574,7 +576,7 @@ def layer1_coarse_filter(): # 过去24h内在screening_log出现过的币,不受"涨太多"过滤限制 # 防止ICP/SUI类:系统早已盯上但被burst_threshold×1.5误挡 import sqlite3 as _sq - _c = _sq.connect(os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db"))) + _c = _sq.connect(os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db"))) _recent = _c.execute(""" SELECT DISTINCT symbol FROM screening_log WHERE scan_time >= datetime('now', '-24 hours') @@ -859,7 +861,7 @@ def layer1_coarse_filter(): # === 舆情共振加权 === try: - from sentiment_monitor import get_sentiment_scores + from app.services.sentiment_monitor import get_sentiment_scores sentiment_cfg = get_screener_section("sentiment") or {} if sentiment_cfg.get("enabled", True): sentiment_scores = get_sentiment_scores() diff --git a/event_driven_screener.py b/app/services/event_driven_screener.py similarity index 97% rename from event_driven_screener.py rename to app/services/event_driven_screener.py index 474bf42..263423a 100644 --- a/event_driven_screener.py +++ b/app/services/event_driven_screener.py @@ -14,6 +14,7 @@ import hashlib import sqlite3 from datetime import datetime, timedelta, timezone from email.utils import parsedate_to_datetime +from pathlib import Path from urllib.parse import quote_plus import ccxt @@ -23,9 +24,9 @@ import yaml sys.path.insert(0, os.path.dirname(__file__)) -from config_loader import load_rules, get_meta, get_strategy_direction -from altcoin_db import init_db, get_conn, create_recommendation, log_screening, log_cron_run, should_push, log_push, get_recommendation_for_push -from altcoin_screener import ( +from app.config.config_loader import load_rules, get_meta, get_strategy_direction +from app.db.altcoin_db import init_db, get_conn, create_recommendation, log_screening, log_cron_run, should_push, log_push, get_recommendation_for_push +from app.services.altcoin_screener import ( fetch_all_tickers, detect_volume_price_fly, detect_static_accumulation, @@ -37,11 +38,12 @@ from altcoin_screener import ( EXCLUDED_BASES, EXCLUDED_BASE_SUFFIXES, ) -from altcoin_confirm import fetch_derivatives_context -from pa_engine import full_pa_analysis, calc_atr -from feishu_push import push_recommendation_state_alert +from app.services.altcoin_confirm import fetch_derivatives_context +from app.core.pa_engine import full_pa_analysis, calc_atr +from app.integrations.feishu_push import push_recommendation_state_alert -DB_PATH = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) +REPO_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) exchange = ccxt.binance({"enableRateLimit": True}) LEVEL_RANK = {"S": 4, "A": 3, "B": 2, "C": 1, "D": 0, "RISK": 5} @@ -308,7 +310,7 @@ def fetch_coingecko_trending_events(): if not cfg.get("enabled", True): return [] try: - from sentiment_monitor import fetch_trending_coins, _get_previous_trending + from app.services.sentiment_monitor import fetch_trending_coins, _get_previous_trending trending = fetch_trending_coins() prev = {r["symbol"] for r in _get_previous_trending()} events = [] diff --git a/price_tracker.py b/app/services/price_tracker.py similarity index 98% rename from price_tracker.py rename to app/services/price_tracker.py index bc4948a..1ef328a 100644 --- a/price_tracker.py +++ b/app/services/price_tracker.py @@ -21,23 +21,25 @@ import json import sys import os from datetime import datetime +from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) -from altcoin_db import ( +from app.db.altcoin_db import ( init_db, get_active_recommendations, update_recommendation_tracking, expire_old_recommendations, get_stats, update_recommendation_action_status, should_push, log_push, apply_recommendation_state_transition, log_cron_run, update_latest_price_cache, ) -from pa_engine import ( +from app.core.pa_engine import ( calc_atr, full_pa_analysis, detect_trend_exhaustion, analyze_entry_point, ) -from feishu_push import push_altcoin_tp_sl_alert -from config_loader import load_rules -from opportunity_lifecycle import apply_entry_quality_gate +from app.integrations.feishu_push import push_altcoin_tp_sl_alert +from app.config.config_loader import load_rules +from app.core.opportunity_lifecycle import apply_entry_quality_gate exchange = ccxt.binance({"enableRateLimit": True}) +REPO_ROOT = Path(__file__).resolve().parents[2] def fetch_klines(symbol, timeframe, limit=200): @@ -316,7 +318,7 @@ def track_prices(): if abs(trail_level - old_trail) > 0.000001: entry_plan["trailing_stop_level"] = trail_level import sqlite3 as _sq - _c2 = _sq.connect(os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db"))) + _c2 = _sq.connect(os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db"))) _c2.execute("UPDATE recommendation SET entry_plan_json=? WHERE id=?", (json.dumps(entry_plan, ensure_ascii=False), rec["id"])) _c2.commit() diff --git a/review_engine.py b/app/services/review_engine.py similarity index 99% rename from review_engine.py rename to app/services/review_engine.py index 4b78b5f..3f68c24 100644 --- a/review_engine.py +++ b/app/services/review_engine.py @@ -16,21 +16,21 @@ from datetime import datetime, timedelta from collections import defaultdict, Counter sys.path.insert(0, os.path.dirname(__file__)) -from altcoin_db import ( +from app.db.altcoin_db import ( get_conn, record_review, update_signal_performance, get_signal_weights, record_missed_explosion, get_review_stats, log_strategy_iteration, upsert_strategy_rule_candidate, record_strategy_failure_pattern, get_strategy_rule_candidates, update_strategy_rule_candidate_status, refresh_strategy_candidate_performance, ) -from pa_engine import classify_candles, calc_atr, full_pa_analysis -from config_loader import ( +from app.core.pa_engine import classify_candles, calc_atr, full_pa_analysis +from app.config.config_loader import ( get_review_params, update_meta, get_learned_rules, add_learned_rule, get_rules_snapshot, diff_rule_snapshots, get_meta, update_signal_weight, promote_candidate_rule_to_learned_rule, bump_strategy_patch_version, ) -import reverse_analysis -import feishu_review_push +from app.analysis import reverse_analysis +from app.integrations import feishu_review_push import requests BINANCE_API = "https://api.binance.com/api/v3" @@ -1340,7 +1340,7 @@ def run_review(): # 通过config_loader更新迭代计数 current_meta = {} try: - from config_loader import get_meta + from app.config.config_loader import get_meta current_meta = get_meta() except: pass @@ -1387,4 +1387,4 @@ def run_review(): if __name__ == "__main__": - run_review() \ No newline at end of file + run_review() diff --git a/sentiment_monitor.py b/app/services/sentiment_monitor.py similarity index 98% rename from sentiment_monitor.py rename to app/services/sentiment_monitor.py index a39ee8c..611ec64 100644 --- a/sentiment_monitor.py +++ b/app/services/sentiment_monitor.py @@ -13,12 +13,14 @@ import requests import xml.etree.ElementTree as ET from datetime import datetime, timedelta from collections import defaultdict +from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) COINGECKO_TRENDING_URL = "https://api.coingecko.com/api/v3/search/trending" GOOGLE_NEWS_RSS = "https://news.google.com/rss/search?q={query}&hl=en-US&gl=US&ceid=US:en" -DB_PATH = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) +REPO_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) def _get_conn(): diff --git a/app/web/__init__.py b/app/web/__init__.py new file mode 100644 index 0000000..0a3074a --- /dev/null +++ b/app/web/__init__.py @@ -0,0 +1 @@ +"""Web application modules.""" diff --git a/web_server.py b/app/web/web_server.py similarity index 99% rename from web_server.py rename to app/web/web_server.py index 99d73f4..1487d38 100644 --- a/web_server.py +++ b/app/web/web_server.py @@ -9,15 +9,14 @@ import json import sqlite3 from datetime import datetime, timezone from contextvars import ContextVar +from pathlib import Path from fastapi import FastAPI, HTTPException, Cookie, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel -import auth_db - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from altcoin_db import ( +from app.db import auth_db +from app.db.altcoin_db import ( init_db, get_active_recommendations, get_active_recommendations_deduped, get_all_recommendations, get_screening_history, get_stats, get_review_stats, get_cron_run_logs, get_cron_run_summary, get_conn, _derive_execution_fields, get_strategy_insights, get_strategy_rule_candidates, @@ -25,10 +24,12 @@ from altcoin_db import ( dry_run_strategy_candidate_performance, backfill_strategy_failure_patterns, generate_candidates_from_review_history, ) -from config_loader import get_signal_weights, get_meta +from app.config.config_loader import get_signal_weights, get_meta + +REPO_ROOT = Path(__file__).resolve().parents[2] app = FastAPI(title="山寨币爆发监控 v11") -templates = Jinja2Templates(directory="static") +templates = Jinja2Templates(directory=str(REPO_ROOT / "static")) _current_request = ContextVar("current_request", default=None) @@ -537,7 +538,7 @@ async def api_cron_summary(hours: int = 24, altcoin_session: str = Cookie(defaul async def api_sentiment(hours: int = 6, altcoin_session: str = Cookie(default="")): """返回舆情监控数据:以消息/事件为主,币种作为关联信息。""" _require_api_user_with_subscription(altcoin_session) - db = os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")) + db = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) conn = sqlite3.connect(db) conn.row_factory = sqlite3.Row @@ -777,7 +778,7 @@ async def api_kline(symbol: str, interval: str = "1d", limit: int = 60, altcoin_ @app.get("/", response_class=HTMLResponse) async def index(): """落地页 — 原始 HTML,无 Jinja2""" - landing_path = os.path.join(os.path.dirname(__file__), "static", "index.html") + landing_path = str(REPO_ROOT / "static" / "index.html") with open(landing_path, "r", encoding="utf-8") as f: return HTMLResponse(content=f.read()) @@ -785,7 +786,7 @@ async def index(): @app.get("/auth", response_class=HTMLResponse) async def auth_page(): """登录/注册页 — 原始 HTML,无 Jinja2""" - auth_path = os.path.join(os.path.dirname(__file__), "static", "auth.html") + auth_path = str(REPO_ROOT / "static" / "auth.html") with open(auth_path, "r", encoding="utf-8") as f: return HTMLResponse(content=f.read()) @@ -1008,7 +1009,11 @@ async def api_strategy_candidates_generate_history(dry_run: bool = False): return result -STOCK_REPORT_TEMPLATE = open(os.path.join(os.path.dirname(__file__), "stock_report_template.html"), "r", encoding="utf-8").read() +STOCK_REPORT_TEMPLATE = open( + REPO_ROOT / "templates" / "stock_report_template.html", + "r", + encoding="utf-8", +).read() HTML_PAGE = r''' @@ -4247,4 +4252,4 @@ async def api_admin_users(search: str = "", offset: int = 0, limit: int = 50, ta async def api_admin_orders(search: str = "", offset: int = 0, limit: int = 50, status: str = "all", altcoin_session: str = Cookie(default="")): _require_admin(altcoin_session) - return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status) \ No newline at end of file + return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status) diff --git a/backtest_result.json b/backtest_result.json deleted file mode 100644 index ee5f7e4..0000000 --- a/backtest_result.json +++ /dev/null @@ -1,1079 +0,0 @@ -{ - "generated_at": "2026-05-05T00:32:03.841228", - "total": 82, - "wins": 26, - "losses": 24, - "expired": 32, - "win_rate": 52.0, - "avg_pnl": 0.84, - "max_win": 10.39, - "max_loss": -4.55, - "details": [ - { - "result": "hit_tp1", - "exit_price": 0.034, - "exit_time": "2026-04-28 15:45:00", - "pnl_pct": 10.39, - "max_profit_pct": 11.36, - "max_loss_pct": 0, - "hours": -6.0, - "symbol": "BIO/USDT", - "rec_time": "2026-04-28T21:43:28.483070", - "entry_price": 0.0308, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 1.36625, - "exit_time": "2026-05-04 01:00:00", - "pnl_pct": 1.05, - "max_profit_pct": 1.26, - "max_loss_pct": -4.29, - "hours": 101.3, - "symbol": "TON/USDT", - "rec_time": "2026-04-29T19:41:58.964829", - "entry_price": 1.352, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.939826, - "exit_time": "2026-04-29 15:00:00", - "pnl_pct": -3.71, - "max_profit_pct": 0.72, - "max_loss_pct": -6.25, - "hours": -4.7, - "symbol": "FIL/USDT", - "rec_time": "2026-04-29T19:42:02.324312", - "entry_price": 0.976, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 1.469, - "exit_time": "2026-05-01 04:15:00", - "pnl_pct": 3.23, - "max_profit_pct": 4.36, - "max_loss_pct": -2.67, - "hours": -3.6, - "symbol": "PENDLE/USDT", - "rec_time": "2026-05-01T07:48:55.631413", - "entry_price": 1.423, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.20585, - "exit_time": "2026-05-02 19:15:00", - "pnl_pct": 2.62, - "max_profit_pct": 2.94, - "max_loss_pct": -1.69, - "hours": 19.3, - "symbol": "FET/USDT", - "rec_time": "2026-05-01T23:57:39.823125", - "entry_price": 0.2006, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.170474, - "exit_time": "2026-05-02 06:00:00", - "pnl_pct": -2.03, - "max_profit_pct": 5.75, - "max_loss_pct": -2.13, - "hours": 2.2, - "symbol": "APE/USDT", - "rec_time": "2026-05-02T03:45:41.275773", - "entry_price": 0.174, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 6.061391, - "exit_time": "2026-05-02 11:30:00", - "pnl_pct": -1.84, - "max_profit_pct": 1.86, - "max_loss_pct": -1.99, - "hours": 4.6, - "symbol": "ZEN/USDT", - "rec_time": "2026-05-02T06:51:21.547178", - "entry_price": 6.175, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.375099, - "exit_time": "2026-05-02 00:00:00", - "pnl_pct": -2.9, - "max_profit_pct": 0.28, - "max_loss_pct": -3.31, - "hours": -7.9, - "symbol": "API3/USDT", - "rec_time": "2026-05-02T07:53:32.407002", - "entry_price": 0.3863, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.009688, - "exit_time": "2026-05-03 01:00:00", - "pnl_pct": -4.55, - "max_profit_pct": 1.18, - "max_loss_pct": -4.73, - "hours": 14.0, - "symbol": "KAT/USDT", - "rec_time": "2026-05-02T10:58:34.267604", - "entry_price": 0.01015, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.010163, - "exit_time": "2026-05-02 15:30:00", - "pnl_pct": 2.81, - "max_profit_pct": 3.65, - "max_loss_pct": -2.1, - "hours": -1.5, - "symbol": "BANANAS31/USDT", - "rec_time": "2026-05-02T17:00:07.930160", - "entry_price": 0.009885, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.791587, - "exit_time": "2026-05-04 00:15:00", - "pnl_pct": -4.51, - "max_profit_pct": 2.65, - "max_loss_pct": -4.7, - "hours": 29.2, - "symbol": "RAY/USDT", - "rec_time": "2026-05-02T19:00:09.991680", - "entry_price": 0.829, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.010309, - "exit_time": "2026-05-03 00:15:00", - "pnl_pct": -1.91, - "max_profit_pct": 1.14, - "max_loss_pct": -2.19, - "hours": 2.7, - "symbol": "MANTRA/USDT", - "rec_time": "2026-05-02T21:30:10.063552", - "entry_price": 0.01051, - "rec_state": "爆发" - }, - { - "result": "hit_tp2", - "exit_price": 0.336272, - "exit_time": "2026-05-03 05:15:00", - "pnl_pct": 1.32, - "max_profit_pct": 1.57, - "max_loss_pct": -0.78, - "hours": 6.7, - "symbol": "TRX/USDT", - "rec_time": "2026-05-02T22:30:05.776861", - "entry_price": 0.3319, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.010781, - "exit_time": "2026-05-03 12:00:00", - "pnl_pct": 3.17, - "max_profit_pct": 3.83, - "max_loss_pct": -5.43, - "hours": 11.5, - "symbol": "BANANAS31/USDT", - "rec_time": "2026-05-03T00:30:04.737548", - "entry_price": 0.01045, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.0794, - "exit_time": "2026-05-03 06:45:00", - "pnl_pct": 7.01, - "max_profit_pct": 11.46, - "max_loss_pct": -1.75, - "hours": 6.2, - "symbol": "AXL/USDT", - "rec_time": "2026-05-03T00:30:08.812555", - "entry_price": 0.0742, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.279464, - "exit_time": "2026-05-03 01:00:00", - "pnl_pct": -2.59, - "max_profit_pct": 0.77, - "max_loss_pct": -2.65, - "hours": 0.5, - "symbol": "ONDO/USDT", - "rec_time": "2026-05-03T00:30:12.078991", - "entry_price": 0.2869, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 278.040107, - "exit_time": "2026-05-04 15:45:00", - "pnl_pct": -3.46, - "max_profit_pct": 2.78, - "max_loss_pct": -3.96, - "hours": 36.2, - "symbol": "TAO/USDT", - "rec_time": "2026-05-03T03:30:05.518143", - "entry_price": 288.0, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.017837, - "exit_time": "2026-05-04 02:30:00", - "pnl_pct": 7.65, - "max_profit_pct": 7.72, - "max_loss_pct": -2.05, - "hours": 16.0, - "symbol": "ZK/USDT", - "rec_time": "2026-05-03T10:30:10.995750", - "entry_price": 0.01657, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.057272, - "exit_time": "2026-05-03 06:30:00", - "pnl_pct": -2.43, - "max_profit_pct": 0.17, - "max_loss_pct": -2.56, - "hours": -4.0, - "symbol": "PNUT/USDT", - "rec_time": "2026-05-03T10:30:12.075008", - "entry_price": 0.0587, - "rec_state": "爆发" - }, - { - "result": "hit_tp2", - "exit_price": 0.340775, - "exit_time": "2026-05-03 05:45:00", - "pnl_pct": -0.18, - "max_profit_pct": 0.7, - "max_loss_pct": -0.67, - "hours": -7.9, - "symbol": "TRX/USDT", - "rec_time": "2026-05-03T13:40:05.714240", - "entry_price": 0.3414, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.33725, - "exit_time": "2026-05-03 06:00:00", - "pnl_pct": -0.72, - "max_profit_pct": 0.35, - "max_loss_pct": -0.5, - "hours": -7.8, - "symbol": "TRX/USDT", - "rec_time": "2026-05-03T13:50:05.154499", - "entry_price": 0.3397, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.3385, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.71, - "max_loss_pct": -0.86, - "hours": 0, - "symbol": "TRX/USDT", - "rec_time": "2026-05-03T14:10:07.208413", - "entry_price": 0.3385, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.169761, - "exit_time": "2026-05-03 10:00:00", - "pnl_pct": -1.07, - "max_profit_pct": 5.3, - "max_loss_pct": -1.17, - "hours": -4.2, - "symbol": "KNC/USDT", - "rec_time": "2026-05-03T14:10:13.150860", - "entry_price": 0.1716, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.168683, - "exit_time": "2026-05-03 15:15:00", - "pnl_pct": -1.53, - "max_profit_pct": 2.45, - "max_loss_pct": -2.22, - "hours": -2.9, - "symbol": "KNC/USDT", - "rec_time": "2026-05-03T18:10:10.733169", - "entry_price": 0.1713, - "rec_state": "爆发" - }, - { - "result": "hit_tp2", - "exit_price": 41.245, - "exit_time": "2026-05-04 00:45:00", - "pnl_pct": 7.27, - "max_profit_pct": 9.67, - "max_loss_pct": -0.86, - "hours": 5.6, - "symbol": "DASH/USDT", - "rec_time": "2026-05-03T19:10:08.624386", - "entry_price": 38.45, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.042407, - "exit_time": "2026-05-03 16:00:00", - "pnl_pct": -2.27, - "max_profit_pct": 1.38, - "max_loss_pct": -2.56, - "hours": -6.8, - "symbol": "NIL/USDT", - "rec_time": "2026-05-03T22:50:17.048250", - "entry_price": 0.04339, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.01554, - "exit_time": "2026-05-04 04:00:00", - "pnl_pct": 5.86, - "max_profit_pct": 7.36, - "max_loss_pct": -2.38, - "hours": 5.2, - "symbol": "MUBARAK/USDT", - "rec_time": "2026-05-03T22:50:18.273465", - "entry_price": 0.01468, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 413.6825, - "exit_time": "2026-05-03 22:00:00", - "pnl_pct": 4.52, - "max_profit_pct": 5.32, - "max_loss_pct": -0.45, - "hours": -1.2, - "symbol": "ZEC/USDT", - "rec_time": "2026-05-03T23:10:21.660739", - "entry_price": 395.81, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.05928, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 3.85, - "max_loss_pct": -2.43, - "hours": 0, - "symbol": "CHIP/USDT", - "rec_time": "2026-05-03T23:10:22.879966", - "entry_price": 0.05928, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 1.519, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.92, - "max_loss_pct": -2.11, - "hours": 0, - "symbol": "CAKE/USDT", - "rec_time": "2026-05-03T23:10:24.251888", - "entry_price": 1.519, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.203475, - "exit_time": "2026-05-04 10:15:00", - "pnl_pct": -2.55, - "max_profit_pct": 1.29, - "max_loss_pct": -3.45, - "hours": 10.2, - "symbol": "FET/USDT", - "rec_time": "2026-05-04T00:00:14.002782", - "entry_price": 0.2088, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.338, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.86, - "max_loss_pct": -0.53, - "hours": 0, - "symbol": "TRX/USDT", - "rec_time": "2026-05-04T00:34:20.183922", - "entry_price": 0.338, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.200941, - "exit_time": "2026-05-04 13:00:00", - "pnl_pct": -3.63, - "max_profit_pct": 1.44, - "max_loss_pct": -3.65, - "hours": 12.4, - "symbol": "FET/USDT", - "rec_time": "2026-05-04T00:34:26.943372", - "entry_price": 0.2085, - "rec_state": "爆发" - }, - { - "result": "hit_tp2", - "exit_price": 42.103572, - "exit_time": "2026-05-04 00:45:00", - "pnl_pct": 8.35, - "max_profit_pct": 8.52, - "max_loss_pct": -1.9, - "hours": 0.2, - "symbol": "DASH/USDT", - "rec_time": "2026-05-04T00:34:27.997164", - "entry_price": 38.86, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 7.01525, - "exit_time": "2026-05-03 22:00:00", - "pnl_pct": 6.29, - "max_profit_pct": 6.53, - "max_loss_pct": -1.35, - "hours": -2.6, - "symbol": "ZEN/USDT", - "rec_time": "2026-05-04T00:34:29.211140", - "entry_price": 6.6, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.05931, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 3.79, - "max_loss_pct": -2.48, - "hours": 0, - "symbol": "CHIP/USDT", - "rec_time": "2026-05-04T00:34:33.341036", - "entry_price": 0.05931, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.071427, - "exit_time": "2026-05-03 23:45:00", - "pnl_pct": -2.29, - "max_profit_pct": 1.64, - "max_loss_pct": -2.33, - "hours": -0.9, - "symbol": "AXL/USDT", - "rec_time": "2026-05-04T00:40:09.884434", - "entry_price": 0.0731, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.3375, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.01, - "max_loss_pct": -0.39, - "hours": 0, - "symbol": "TRX/USDT", - "rec_time": "2026-05-04T00:41:30.766064", - "entry_price": 0.3375, - "rec_state": "爆发" - }, - { - "result": "hit_tp2", - "exit_price": 41.994285, - "exit_time": "2026-05-04 00:45:00", - "pnl_pct": 8.88, - "max_profit_pct": 9.33, - "max_loss_pct": -1.17, - "hours": 0.1, - "symbol": "DASH/USDT", - "rec_time": "2026-05-04T00:41:37.151553", - "entry_price": 38.57, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 6.995, - "exit_time": "2026-05-03 22:00:00", - "pnl_pct": 6.71, - "max_profit_pct": 7.26, - "max_loss_pct": -0.67, - "hours": -2.7, - "symbol": "ZEN/USDT", - "rec_time": "2026-05-04T00:41:38.069398", - "entry_price": 6.555, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.005119, - "exit_time": "2026-05-03 23:15:00", - "pnl_pct": -3.05, - "max_profit_pct": 1.14, - "max_loss_pct": -6.06, - "hours": -2.4, - "symbol": "REZ/USDT", - "rec_time": "2026-05-04T01:40:14.833267", - "entry_price": 0.00528, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.031455, - "exit_time": "2026-05-03 20:15:00", - "pnl_pct": -3.13, - "max_profit_pct": 0.62, - "max_loss_pct": -4.19, - "hours": -5.6, - "symbol": "NIGHT/USDT", - "rec_time": "2026-05-04T01:50:14.291182", - "entry_price": 0.03247, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.29605, - "exit_time": "2026-05-04 02:15:00", - "pnl_pct": 0.25, - "max_profit_pct": 0.54, - "max_loss_pct": -3.69, - "hours": -1.6, - "symbol": "ONDO/USDT", - "rec_time": "2026-05-04T03:50:10.309687", - "entry_price": 0.2953, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 2.144023, - "exit_time": "2026-05-03 23:15:00", - "pnl_pct": -2.99, - "max_profit_pct": 0, - "max_loss_pct": -3.17, - "hours": -6.1, - "symbol": "AR/USDT", - "rec_time": "2026-05-04T05:20:16.931016", - "entry_price": 2.21, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.010302, - "exit_time": "2026-05-04 01:45:00", - "pnl_pct": 2.23, - "max_profit_pct": 3.41, - "max_loss_pct": -3.22, - "hours": -5.1, - "symbol": "PENGU/USDT", - "rec_time": "2026-05-04T06:50:17.622742", - "entry_price": 0.010077, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 85.87, - "exit_time": "2026-05-04 04:30:00", - "pnl_pct": 1.49, - "max_profit_pct": 1.52, - "max_loss_pct": -1.49, - "hours": -2.7, - "symbol": "SOL/USDT", - "rec_time": "2026-05-04T07:10:08.775877", - "entry_price": 84.61, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.00516, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 8.72, - "max_loss_pct": -2.13, - "hours": 0, - "symbol": "REZ/USDT", - "rec_time": "2026-05-04T07:40:16.477137", - "entry_price": 0.00516, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 2.15, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 6.05, - "max_loss_pct": -1.86, - "hours": 0, - "symbol": "AR/USDT", - "rec_time": "2026-05-04T07:40:18.852440", - "entry_price": 2.15, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.112077, - "exit_time": "2026-05-04 01:45:00", - "pnl_pct": 1.34, - "max_profit_pct": 2.49, - "max_loss_pct": 0, - "hours": -7.8, - "symbol": "DOGE/USDT", - "rec_time": "2026-05-04T09:30:11.771174", - "entry_price": 0.11059, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 1.4074, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.96, - "max_loss_pct": -1.55, - "hours": 0, - "symbol": "XRP/USDT", - "rec_time": "2026-05-04T10:00:10.923372", - "entry_price": 1.4074, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.01013, - "exit_time": "2026-05-04 06:00:00", - "pnl_pct": -3.49, - "max_profit_pct": 0.3, - "max_loss_pct": -4.02, - "hours": -4.0, - "symbol": "PENGU/USDT", - "rec_time": "2026-05-04T10:00:19.015547", - "entry_price": 0.010496, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.05977, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.05, - "max_loss_pct": -2.78, - "hours": 0, - "symbol": "SEI/USDT", - "rec_time": "2026-05-04T10:00:24.110808", - "entry_price": 0.05977, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 3.379, - "exit_time": "2026-05-04 04:15:00", - "pnl_pct": 2.3, - "max_profit_pct": 2.72, - "max_loss_pct": -0.33, - "hours": -5.9, - "symbol": "UNI/USDT", - "rec_time": "2026-05-04T10:10:09.250285", - "entry_price": 3.303, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.95, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 2.63, - "max_loss_pct": -2.84, - "hours": 0, - "symbol": "FIL/USDT", - "rec_time": "2026-05-04T10:10:14.944443", - "entry_price": 0.95, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.1082, - "exit_time": "2026-05-04 04:15:00", - "pnl_pct": 3.74, - "max_profit_pct": 3.74, - "max_loss_pct": -0.19, - "hours": -5.9, - "symbol": "ENA/USDT", - "rec_time": "2026-05-04T10:10:19.599035", - "entry_price": 0.1043, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.74845, - "exit_time": "2026-05-04 10:00:00", - "pnl_pct": -3.16, - "max_profit_pct": 0.83, - "max_loss_pct": -3.55, - "hours": -0.2, - "symbol": "VIRTUAL/USDT", - "rec_time": "2026-05-04T10:10:29.649474", - "entry_price": 0.7729, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 56.09, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.61, - "max_loss_pct": -2.03, - "hours": 0, - "symbol": "LTC/USDT", - "rec_time": "2026-05-04T10:40:08.122908", - "entry_price": 56.09, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 0.3088, - "exit_time": "2026-05-04 05:45:00", - "pnl_pct": 4.47, - "max_profit_pct": 6.19, - "max_loss_pct": 0, - "hours": -4.9, - "symbol": "ONDO/USDT", - "rec_time": "2026-05-04T10:40:18.706520", - "entry_price": 0.2956, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.3749, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.31, - "max_loss_pct": -3.63, - "hours": 0, - "symbol": "LDO/USDT", - "rec_time": "2026-05-04T10:50:30.331824", - "entry_price": 0.3749, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.983404, - "exit_time": "2026-05-04 10:00:00", - "pnl_pct": -3.02, - "max_profit_pct": 0.69, - "max_loss_pct": -4.83, - "hours": -1.0, - "symbol": "APT/USDT", - "rec_time": "2026-05-04T11:00:07.989259", - "entry_price": 1.014, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.089, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.66, - "max_loss_pct": -1.75, - "hours": 0, - "symbol": "HBAR/USDT", - "rec_time": "2026-05-04T11:00:17.031416", - "entry_price": 0.089, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.0742, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.81, - "max_loss_pct": -3.1, - "hours": 0, - "symbol": "SAND/USDT", - "rec_time": "2026-05-04T11:00:23.817040", - "entry_price": 0.0742, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.2422, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.74, - "max_loss_pct": -3.18, - "hours": 0, - "symbol": "WLD/USDT", - "rec_time": "2026-05-04T11:00:27.432854", - "entry_price": 0.2422, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.939, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 2.1, - "max_loss_pct": -2.09, - "hours": 0, - "symbol": "SUI/USDT", - "rec_time": "2026-05-04T11:10:12.711611", - "entry_price": 0.939, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.1607, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.56, - "max_loss_pct": -2.55, - "hours": 0, - "symbol": "XLM/USDT", - "rec_time": "2026-05-04T11:10:21.413788", - "entry_price": 0.1607, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 1.255362, - "exit_time": "2026-05-04 10:00:00", - "pnl_pct": -3.28, - "max_profit_pct": 1.08, - "max_loss_pct": -3.47, - "hours": -1.2, - "symbol": "NEAR/USDT", - "rec_time": "2026-05-04T11:10:33.850174", - "entry_price": 1.298, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 1.243, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.61, - "max_loss_pct": -3.06, - "hours": 0, - "symbol": "DOT/USDT", - "rec_time": "2026-05-04T11:40:21.842571", - "entry_price": 1.243, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.11294, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.59, - "max_loss_pct": -3.68, - "hours": 0, - "symbol": "DOGE/USDT", - "rec_time": "2026-05-04T12:10:17.732785", - "entry_price": 0.11294, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 3.382, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 2.1, - "max_loss_pct": -3.96, - "hours": 0, - "symbol": "UNI/USDT", - "rec_time": "2026-05-04T12:20:07.109022", - "entry_price": 3.382, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.010077, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.56, - "max_loss_pct": -2.37, - "hours": 0, - "symbol": "PENGU/USDT", - "rec_time": "2026-05-04T14:10:26.558343", - "entry_price": 0.010077, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.0846, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 8.39, - "max_loss_pct": -1.42, - "hours": 0, - "symbol": "AXL/USDT", - "rec_time": "2026-05-04T14:50:16.271065", - "entry_price": 0.0846, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 9.59, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.63, - "max_loss_pct": -3.13, - "hours": 0, - "symbol": "LINK/USDT", - "rec_time": "2026-05-04T17:40:11.514181", - "entry_price": 9.59, - "rec_state": "爆发" - }, - { - "result": "stopped_out", - "exit_price": 0.018842, - "exit_time": "2026-05-04 10:15:00", - "pnl_pct": -2.78, - "max_profit_pct": 0, - "max_loss_pct": -3.97, - "hours": -7.8, - "symbol": "BROCCOLI714/USDT", - "rec_time": "2026-05-04T18:00:30.545500", - "entry_price": 0.01938, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.972, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 2.57, - "max_loss_pct": -1.13, - "hours": 0, - "symbol": "APT/USDT", - "rec_time": "2026-05-04T18:10:06.061513", - "entry_price": 0.972, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.747, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.58, - "max_loss_pct": -1.53, - "hours": 0, - "symbol": "VIRTUAL/USDT", - "rec_time": "2026-05-04T18:10:27.994121", - "entry_price": 0.747, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.01885, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 7.43, - "max_loss_pct": -1.27, - "hours": 0, - "symbol": "BROCCOLI714/USDT", - "rec_time": "2026-05-04T18:10:35.057483", - "entry_price": 0.01885, - "rec_state": "爆发" - }, - { - "result": "hit_tp1", - "exit_price": 1.82525, - "exit_time": "2026-05-04 14:45:00", - "pnl_pct": 8.07, - "max_profit_pct": 8.58, - "max_loss_pct": 0, - "hours": -3.6, - "symbol": "PENDLE/USDT", - "rec_time": "2026-05-04T18:20:24.088266", - "entry_price": 1.689, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.01753, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.91, - "max_loss_pct": -2.74, - "hours": 0, - "symbol": "ZK/USDT", - "rec_time": "2026-05-04T18:20:28.751726", - "entry_price": 0.01753, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.06046, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0, - "max_loss_pct": -2.38, - "hours": 0, - "symbol": "CHIP/USDT", - "rec_time": "2026-05-04T20:40:25.393372", - "entry_price": 0.06046, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.01804, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 0.5, - "max_loss_pct": -7.59, - "hours": 0, - "symbol": "MUBARAK/USDT", - "rec_time": "2026-05-04T21:50:24.255589", - "entry_price": 0.01804, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 0.0161, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.24, - "max_loss_pct": -3.73, - "hours": 0, - "symbol": "ACT/USDT", - "rec_time": "2026-05-04T22:40:40.835379", - "entry_price": 0.0161, - "rec_state": "爆发" - }, - { - "result": "expired", - "exit_price": 1.439, - "exit_time": "", - "pnl_pct": 0.0, - "max_profit_pct": 1.04, - "max_loss_pct": -1.74, - "hours": 0, - "symbol": "TON/USDT", - "rec_time": "2026-05-04T23:00:18.746105", - "entry_price": 1.439, - "rec_state": "爆发" - } - ] -} \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index ccfc05b..94db7a0 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -12,7 +12,7 @@ fi case "${1:-web}" in web) - exec python -m uvicorn web_server:app --host 0.0.0.0 --port "${PORT:-8190}" + exec python -m uvicorn app.web.web_server:app --host 0.0.0.0 --port "${PORT:-8190}" ;; scheduler) exec python /app/docker/scheduler.py diff --git a/docker/scheduler.py b/docker/scheduler.py index 6d8130f..4eab4e3 100755 --- a/docker/scheduler.py +++ b/docker/scheduler.py @@ -25,7 +25,7 @@ DRY_RUN = os.getenv("ALPHAX_SCHEDULER_DRY_RUN", "1").strip() not in {"0", "false @dataclass class Job: name: str - script: str + module: str every_seconds: int args: tuple[str, ...] = () initial_delay: int = 0 @@ -43,7 +43,7 @@ def env_for_child() -> dict[str, str]: def run_job(job: Job) -> None: - cmd = [PYTHON, str(ROOT / job.script), *job.args] + cmd = [PYTHON, "-m", job.module, *job.args] print(f"[{now_str()}] [scheduler] start {job.name}: {' '.join(cmd)}", flush=True) if DRY_RUN: print(f"[{now_str()}] [scheduler] DRY_RUN=1 skip {job.name}", flush=True) @@ -75,12 +75,12 @@ def run_job(job: Job) -> None: def build_jobs() -> list[Job]: # 与当前宿主机 crontab 对齐,但串行执行。 return [ - Job("event", "event_driven_screener.py", 60, ("--once",), initial_delay=5), - Job("tracker", "price_tracker.py", 180, initial_delay=20), - Job("confirm", "altcoin_confirm.py", 600, initial_delay=40), - Job("screener", "altcoin_screener.py", 900, initial_delay=80), - Job("sentiment", "sentiment_monitor.py", 1800, ("--collect",), initial_delay=120), - Job("review", "review_engine.py", 24 * 3600, initial_delay=300), + Job("event", "app.services.event_driven_screener", 60, ("--once",), initial_delay=5), + Job("tracker", "app.services.price_tracker", 180, initial_delay=20), + Job("confirm", "app.services.altcoin_confirm", 600, initial_delay=40), + Job("screener", "app.services.altcoin_screener", 900, initial_delay=80), + Job("sentiment", "app.services.sentiment_monitor", 1800, ("--collect",), initial_delay=120), + Job("review", "app.services.review_engine", 24 * 3600, initial_delay=300), ] diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..d28560b --- /dev/null +++ b/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,122 @@ +# Project Structure + +## 目标 + +这次整理的目标不是把项目“重写成新架构”,而是先把目录语义拉清楚: + +- 根目录只放主运行链路和顶层配置 +- 工具脚本集中到 `tools/` +- 校验脚本集中到 `scripts/` +- 模板资源集中到 `templates/` +- 运行/分析产物集中到 `reports/` +- 历史遗留与备份集中到 `legacy/` + +## 当前建议心智模型 + +### 1. 真实实现层 + +真实实现现在集中在 `app/` 下: + +- `app/services/` +- `app/db/` +- `app/core/` +- `app/config/` +- `app/integrations/` +- `app/analysis/` +- `app/web/` + +### 2. 运行主链路 + +- `app/services/altcoin_screener.py` +- `app/services/altcoin_confirm.py` +- `app/services/price_tracker.py` +- `app/services/event_driven_screener.py` +- `app/services/review_engine.py` +- `app/web/web_server.py` + +### 3. 核心共享层 + +- `app/db/altcoin_db.py` +- `app/db/auth_db.py` +- `app/config/config_loader.py` +- `app/core/opportunity_lifecycle.py` +- `app/core/pa_engine.py` +- `app/core/sector_map.py` +- `app/integrations/feishu_push.py` +- `app/integrations/feishu_review_push.py` + +### 4. 配置与部署 + +- `rules.yaml` +- `.env.example` +- `Dockerfile` +- `docker-compose.yml` +- `docker/` + +### 5. 前端与模板 + +- `static/` +- `templates/` + +### 6. 开发辅助 + +- `tests/` +- `scripts/` +- `tools/` +- `docs/` + +### 7. 非主路径归档 + +- `legacy/` +- `reports/` + +## 已整理的文件 + +### 移入 `tools/` + +- `backtest.py` -> `tools/backtest.py` +- `extract_summary.py` -> `tools/extract_summary.py` +- `summarize_output.py` -> `tools/summarize_output.py` + +### 移入 `scripts/` + +- `validate_params.py` -> `scripts/validate_params.py` + +### 移入 `templates/` + +- `stock_report_template.html` -> `templates/stock_report_template.html` + +### 移入 `reports/` + +- `backtest_result.json` -> `reports/backtest_result.json` + +### 移入 `docs/reference/` + +- `schema.py` -> `docs/reference/schema_reference.py` + +### 移入 `legacy/` + +- `coin_state_tracker.py` +- `price_tracker_ws.py` +- `legacy/web/index.html` +- `legacy/static/app.html.bak` +- `legacy/scratch/*` + +## 后续建议 + +如果继续整理,建议下一步做实现层拆分,而不是再搬目录: + +1. 拆 `web_server.py` +2. 拆 `altcoin_db.py` +3. 为 `rules.yaml` 增加 schema 校验 +4. 为主脚本建立统一 CLI 入口 + +## 当前结论 + +这次整理后: + +- `app/` 承载真实实现 +- 根目录不再堆放业务实现脚本 +- 旧工具/旧资产/产物不再继续污染主目录 + +这不是最终态,但已经从“所有实现都摊在根目录”进到了“实现按职责分层”的可维护阶段。 diff --git a/schema.py b/docs/reference/schema_reference.py similarity index 95% rename from schema.py rename to docs/reference/schema_reference.py index 57570a3..3975125 100644 --- a/schema.py +++ b/docs/reference/schema_reference.py @@ -11,9 +11,12 @@ 此文件不应被 screener/confirm 等模块直接导入调用。 """ +import os import sqlite3 +from pathlib import Path -DB_PATH = "/home/ubuntu/quant_monitor/altcoin/altcoin_monitor.db" +REPO_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) def init_db(): @@ -113,4 +116,4 @@ def init_db(): if __name__ == "__main__": init_db() - print("DB Schema初始化完成(4张表+5个索引)") \ No newline at end of file + print("DB Schema初始化完成(4张表+5个索引)") diff --git a/coin_state_tracker.py b/legacy/coin_state_tracker.py similarity index 100% rename from coin_state_tracker.py rename to legacy/coin_state_tracker.py diff --git a/price_tracker_ws.py b/legacy/price_tracker_ws.py similarity index 98% rename from price_tracker_ws.py rename to legacy/price_tracker_ws.py index cf3cb02..bae0a39 100644 --- a/price_tracker_ws.py +++ b/legacy/price_tracker_ws.py @@ -13,7 +13,7 @@ from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from altcoin_db import ( +from app.db.altcoin_db import ( init_db, get_active_recommendations_deduped, update_recommendation_tracking, @@ -21,7 +21,7 @@ from altcoin_db import ( should_push, log_push, ) -from feishu_push import push_altcoin_tp_sl_alert +from app.integrations.feishu_push import push_altcoin_tp_sl_alert POLL_INTERVAL = 5 REFRESH_INTERVAL = 60 diff --git a/web/index.html b/legacy/web/index.html similarity index 100% rename from web/index.html rename to legacy/web/index.html diff --git a/rules.yaml b/rules.yaml index f933597..7851f6e 100644 --- a/rules.yaml +++ b/rules.yaml @@ -986,8 +986,8 @@ monitoring: min_score_max: 6 require_human_if_exceeded: true param_audit: - description: 参数变更审计:每次复盘运行validate_params.py验证关键参数未被意外修改 - validate_script: validate_params.py + description: 参数变更审计:每次复盘运行scripts/validate_params.py验证关键参数未被意外修改 + validate_script: scripts/validate_params.py hash_algorithm: semantic_sha256 critical_sections: - confirm diff --git a/validate_params.py b/scripts/validate_params.py similarity index 92% rename from validate_params.py rename to scripts/validate_params.py index cb5ba67..9fa20a9 100644 --- a/validate_params.py +++ b/scripts/validate_params.py @@ -1,9 +1,13 @@ #!/usr/bin/env python3 """参数变更审计:检测 rules.yaml 是否被意外修改""" -import hashlib, yaml, sys +import hashlib +import sys from pathlib import Path -RULES_PATH = Path(__file__).parent / 'rules.yaml' +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] +RULES_PATH = REPO_ROOT / "rules.yaml" def compute_semantic_hash(rules_dict): """语义哈希:忽略格式差异,只对关键参数字段做哈希""" diff --git a/stock_report_template.html b/templates/stock_report_template.html similarity index 100% rename from stock_report_template.html rename to templates/stock_report_template.html diff --git a/tests/test_actionable_active_recommendations.py b/tests/test_actionable_active_recommendations.py index 96e1ba8..be4beaf 100644 --- a/tests/test_actionable_active_recommendations.py +++ b/tests/test_actionable_active_recommendations.py @@ -8,8 +8,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server @pytest.fixture @@ -340,4 +340,3 @@ def test_version_filter_labels_use_plan_not_executable_to_avoid_wait_pullback_co assert '历史推荐' in html # v1.7.7: 新看板版本下拉用 (${v.count}) 格式 + 默认选中最新版本 assert 'v.count' in html - diff --git a/tests/test_confirm_freshness_gate.py b/tests/test_confirm_freshness_gate.py index 82475f7..1731f5c 100644 --- a/tests/test_confirm_freshness_gate.py +++ b/tests/test_confirm_freshness_gate.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from altcoin_confirm import _is_candidate_fresh, _event_time_from_age +from app.services.altcoin_confirm import _is_candidate_fresh, _event_time_from_age def test_candidate_fresh_when_state_detected_recently_without_current_trigger(): diff --git a/tests/test_cron_run_logs.py b/tests/test_cron_run_logs.py index bba5644..2cddbca 100644 --- a/tests/test_cron_run_logs.py +++ b/tests/test_cron_run_logs.py @@ -8,8 +8,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server @pytest.fixture diff --git a/tests/test_event_driven_screener.py b/tests/test_event_driven_screener.py index 1d13a9c..76aa56b 100644 --- a/tests/test_event_driven_screener.py +++ b/tests/test_event_driven_screener.py @@ -8,7 +8,7 @@ import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -import event_driven_screener as ed +from app.services import event_driven_screener as ed def test_symbol_extraction_filters_usdt_suffix_and_pollution(): diff --git a/tests/test_executed_trade_pnl_semantics.py b/tests/test_executed_trade_pnl_semantics.py index 283fbdd..55d1308 100644 --- a/tests/test_executed_trade_pnl_semantics.py +++ b/tests/test_executed_trade_pnl_semantics.py @@ -8,8 +8,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server from test_actionable_active_recommendations import _insert_recommendation diff --git a/tests/test_higher_lows_compression.py b/tests/test_higher_lows_compression.py index 432c1e3..64b3d61 100644 --- a/tests/test_higher_lows_compression.py +++ b/tests/test_higher_lows_compression.py @@ -22,7 +22,7 @@ def make_df(highs, lows, closes, opens=None, volumes=None): def test_detect_higher_lows_finds_clear_pattern(): """验证底部抬高检测能识别清晰的逐步抬高低点""" - from altcoin_screener import detect_higher_lows + from app.services.altcoin_screener import detect_higher_lows lows, highs, closes, volumes = [], [], [], [] for seg in range(6): @@ -44,7 +44,7 @@ def test_detect_higher_lows_finds_clear_pattern(): def test_detect_higher_lows_rejects_declining_trend(): """验证下降趋势不会被误判为底部抬高""" - from altcoin_screener import detect_higher_lows + from app.services.altcoin_screener import detect_higher_lows lows, highs, closes, volumes = [], [], [], [] for seg in range(6): @@ -64,7 +64,7 @@ def test_detect_higher_lows_rejects_declining_trend(): def test_detect_compression_surge_detects_tight_then_volume(): """验证压缩后放量模式能被检测""" - from altcoin_screener import detect_compression_surge + from app.services.altcoin_screener import detect_compression_surge lows = [100 + np.random.uniform(-0.5, 0.5) for _ in range(24)] highs = [l + np.random.uniform(1, 2.5) for l in lows] @@ -82,7 +82,7 @@ def test_detect_compression_surge_detects_tight_then_volume(): def test_detect_compression_surge_rejects_wide_range(): """验证宽幅震荡不触发压缩检测""" - from altcoin_screener import detect_compression_surge + from app.services.altcoin_screener import detect_compression_surge lows = [np.random.uniform(80, 120) for _ in range(24)] highs = [l + np.random.uniform(5, 15) for l in lows] @@ -97,7 +97,7 @@ def test_detect_compression_surge_rejects_wide_range(): def test_signal_deprecation_config_exists(): """验证信号淘汰机制配置可正确读取""" - from config_loader import get_review_params + from app.config.config_loader import get_review_params dep = get_review_params().get("signal_deprecation", {}) assert dep.get("enabled") is True diff --git a/tests/test_history_grouping.py b/tests/test_history_grouping.py index a9402ea..7962ec1 100644 --- a/tests/test_history_grouping.py +++ b/tests/test_history_grouping.py @@ -10,7 +10,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db +from app.db import altcoin_db class RecommendationHistoryBase(unittest.TestCase): diff --git a/tests/test_iteration_diff_effect.py b/tests/test_iteration_diff_effect.py index 5496428..455d234 100644 --- a/tests/test_iteration_diff_effect.py +++ b/tests/test_iteration_diff_effect.py @@ -9,8 +9,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server @pytest.fixture diff --git a/tests/test_market_context_enrichment.py b/tests/test_market_context_enrichment.py index 5567051..48606b1 100644 --- a/tests/test_market_context_enrichment.py +++ b/tests/test_market_context_enrichment.py @@ -9,8 +9,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server @pytest.fixture() diff --git a/tests/test_opportunity_lifecycle.py b/tests/test_opportunity_lifecycle.py index cb9ec11..2b5ba1d 100644 --- a/tests/test_opportunity_lifecycle.py +++ b/tests/test_opportunity_lifecycle.py @@ -4,8 +4,8 @@ import sys sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from opportunity_lifecycle import apply_entry_quality_gate -import price_tracker_ws +from app.core.opportunity_lifecycle import apply_entry_quality_gate +from legacy import price_tracker_ws def test_risk_reward_false_blocks_buy_now(): diff --git a/tests/test_pa_recency.py b/tests/test_pa_recency.py index 6ec2a23..0905ea2 100644 --- a/tests/test_pa_recency.py +++ b/tests/test_pa_recency.py @@ -5,7 +5,7 @@ import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from pa_engine import calc_atr, classify_candles, detect_ignition_point +from app.core.pa_engine import calc_atr, classify_candles, detect_ignition_point def _ignition_df(stale_age_bars=6): diff --git a/tests/test_personalization_strategy_stage2_3.py b/tests/test_personalization_strategy_stage2_3.py index c684c64..b1d6c44 100644 --- a/tests/test_personalization_strategy_stage2_3.py +++ b/tests/test_personalization_strategy_stage2_3.py @@ -8,8 +8,8 @@ from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -import auth_db -import altcoin_db +from app.db import auth_db +from app.db import altcoin_db class PersonalizationAndStrategyInsightTests(unittest.TestCase): diff --git a/tests/test_recommendation_execution_status.py b/tests/test_recommendation_execution_status.py index 1af0591..03b4321 100644 --- a/tests/test_recommendation_execution_status.py +++ b/tests/test_recommendation_execution_status.py @@ -5,7 +5,7 @@ import tempfile import unittest from unittest.mock import patch -import altcoin_db +from app.db import altcoin_db class RecommendationExecutionStatusTests(unittest.TestCase): diff --git a/tests/test_recommendation_state_mainline.py b/tests/test_recommendation_state_mainline.py index 774b259..e09438a 100644 --- a/tests/test_recommendation_state_mainline.py +++ b/tests/test_recommendation_state_mainline.py @@ -8,7 +8,7 @@ from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -import altcoin_db +from app.db import altcoin_db class RecommendationStateMainlineTests(unittest.TestCase): diff --git a/tests/test_replay_validation.py b/tests/test_replay_validation.py index e5f665c..a0bd88e 100644 --- a/tests/test_replay_validation.py +++ b/tests/test_replay_validation.py @@ -5,7 +5,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_screener +from app.services import altcoin_screener def test_replay_samples_cover_pnt_cream_ai(): diff --git a/tests/test_review_page_slimming.py b/tests/test_review_page_slimming.py index 3bd05ba..ff9a104 100644 --- a/tests/test_review_page_slimming.py +++ b/tests/test_review_page_slimming.py @@ -7,7 +7,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import web_server +from app.web import web_server def test_index_hides_screening_from_top_level_tabs(): diff --git a/tests/test_review_version_summary.py b/tests/test_review_version_summary.py index f0e01b1..388002c 100644 --- a/tests/test_review_version_summary.py +++ b/tests/test_review_version_summary.py @@ -5,7 +5,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db +from app.db import altcoin_db def test_review_stats_contains_strategy_version_summary_and_changelog(monkeypatch): diff --git a/tests/test_screener_optimizations.py b/tests/test_screener_optimizations.py index 0a6121e..27cc631 100644 --- a/tests/test_screener_optimizations.py +++ b/tests/test_screener_optimizations.py @@ -7,7 +7,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_screener +from app.services import altcoin_screener def test_fetch_all_tickers_filters_stable_and_fiat_suffixes(monkeypatch): diff --git a/tests/test_signal_trust_stage1.py b/tests/test_signal_trust_stage1.py index d00ed67..66c1cd6 100644 --- a/tests/test_signal_trust_stage1.py +++ b/tests/test_signal_trust_stage1.py @@ -9,7 +9,7 @@ from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -import altcoin_db +from app.db import altcoin_db class RecommendationSignalTrustTests(unittest.TestCase): diff --git a/tests/test_strategy_iteration_logs.py b/tests/test_strategy_iteration_logs.py index ed366cc..658519d 100644 --- a/tests/test_strategy_iteration_logs.py +++ b/tests/test_strategy_iteration_logs.py @@ -9,8 +9,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import web_server +from app.db import altcoin_db +from app.web import web_server @pytest.fixture diff --git a/tests/test_strategy_revision_marker.py b/tests/test_strategy_revision_marker.py index d796685..39b63e7 100644 --- a/tests/test_strategy_revision_marker.py +++ b/tests/test_strategy_revision_marker.py @@ -8,9 +8,9 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import review_engine -import config_loader +from app.db import altcoin_db +from app.services import review_engine +from app.config import config_loader @pytest.fixture diff --git a/tests/test_strategy_version.py b/tests/test_strategy_version.py index 987278f..8cbb0b3 100644 --- a/tests/test_strategy_version.py +++ b/tests/test_strategy_version.py @@ -6,7 +6,7 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db +from app.db import altcoin_db def test_strategy_version_from_meta(): diff --git a/tests/test_tracker_terminal_action_guard.py b/tests/test_tracker_terminal_action_guard.py index 7a1d7cc..bc3a5b0 100644 --- a/tests/test_tracker_terminal_action_guard.py +++ b/tests/test_tracker_terminal_action_guard.py @@ -5,8 +5,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import altcoin_db -import price_tracker_ws +from app.db import altcoin_db +from legacy import price_tracker_ws def test_terminal_recommendation_action_status_cannot_be_overwritten_by_entry_signal(monkeypatch, tmp_path): diff --git a/tests/test_user_subscription_auth.py b/tests/test_user_subscription_auth.py index ea9fad1..2750299 100644 --- a/tests/test_user_subscription_auth.py +++ b/tests/test_user_subscription_auth.py @@ -9,8 +9,8 @@ PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) -import auth_db -import web_server +from app.db import auth_db +from app.web import web_server @pytest.fixture diff --git a/tests/test_vp_fly_recency.py b/tests/test_vp_fly_recency.py index 11c2a9b..a948831 100644 --- a/tests/test_vp_fly_recency.py +++ b/tests/test_vp_fly_recency.py @@ -5,8 +5,8 @@ import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from altcoin_screener import detect_volume_price_fly -from altcoin_confirm import detect_volume_price_fly_1h +from app.services.altcoin_screener import detect_volume_price_fly +from app.services.altcoin_confirm import detect_volume_price_fly_1h def _sample_df(stale_age_hours=9): diff --git a/backtest.py b/tools/backtest.py similarity index 93% rename from backtest.py rename to tools/backtest.py index 6167e84..213b08d 100644 --- a/backtest.py +++ b/tools/backtest.py @@ -2,17 +2,22 @@ 山寨币策略回测脚本 对 DB 中所有有完整入场方案(stop_loss/tp1/tp2)的推荐做模拟跟踪。 """ -import sys, os, json, sqlite3 -from datetime import datetime, timedelta - -sys.path.insert(0, '/home/ubuntu/quant_monitor/altcoin') +import json +import os +import sqlite3 +import sys +from datetime import datetime +from pathlib import Path import ccxt import pandas as pd -import numpy as np + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) exchange = ccxt.binance({'enableRateLimit': True}) -DB = '/home/ubuntu/quant_monitor/altcoin/altcoin_monitor.db' +DB = os.getenv("ALPHAX_DB_PATH", str(REPO_ROOT / "data" / "altcoin_monitor.db")) +OUTPUT_PATH = REPO_ROOT / "reports" / "backtest_result.json" def fetch_klines_since(symbol, timeframe, since_ms, limit=500): @@ -51,7 +56,6 @@ def simulate_trade(rec, klines_df): close = float(row['close']) ts = row['timestamp'] - current_pnl = (close / entry_price - 1) * 100 max_profit_pct = max(max_profit_pct, (high / entry_price - 1) * 100) max_loss_pct = min(max_loss_pct, (low / entry_price - 1) * 100) @@ -179,7 +183,8 @@ def main(): f"{r['pnl_pct']:>6.1f}% {r['max_profit_pct']:>6.1f}% {r['max_loss_pct']:>6.1f}% {r['hours']:>5.1f}") # Save to JSON for HTML report - with open('/home/ubuntu/quant_monitor/altcoin/backtest_result.json', 'w') as f: + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, 'w', encoding='utf-8') as f: json.dump({ 'generated_at': datetime.now().isoformat(), 'total': len(results), @@ -193,7 +198,7 @@ def main(): 'details': [{k: str(v) if isinstance(v, (datetime, pd.Timestamp)) else v for k, v in r.items()} for r in results], }, f, ensure_ascii=False, indent=2, default=str) - print(f"\n结果已保存: backtest_result.json") + print(f"\n结果已保存: {OUTPUT_PATH}") if __name__ == '__main__': diff --git a/extract_summary.py b/tools/extract_summary.py similarity index 100% rename from extract_summary.py rename to tools/extract_summary.py diff --git a/summarize_output.py b/tools/summarize_output.py similarity index 100% rename from summarize_output.py rename to tools/summarize_output.py