diff --git a/README.md b/README.md index af5b33a..58c6a7e 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,109 @@ # cbpred -这是一个统一模型的 8H U 本位 USDT 合约回调预测管线。它只使用 banbot 的 K 线 OHLCV 数据,默认调用 banbot 下载 1H 存储数据,再按完整的 8 根 1H K 线聚合为 8H;不使用资金费率、盘口、持仓量或外部行情。 +`cbpred` 是一个面向 Binance U 本位 USDT 永续合约的统一 8H K 线回撤预测管线。数据由 [banbot](../banbot) 下载,模型只使用 OHLCV,不使用资金费率、盘口、持仓量或外部行情。 -目标定义为有符号的未来最低价收益,并按品种训练期波动尺度归一化: +当前正式模型只有一个:[`artifacts/v1/model.joblib`](artifacts/v1/model.joblib)。它由最近 18 根 8H K 线预测未来 6 根 8H K 线的稳健低点收益,并输出点预测以及用于区间参考的 q20/q80 结果。根目录不放置历史实验模型;历史实验说明保留在 [`docs/v0_0823.md`](docs/v0_0823.md) 和 [`docs/v1_0823.md`](docs/v1_0823.md)。 + +## 快速开始 + +运行环境需要 Python 3、Go、可访问的 banbot 源码和 banbot 配置中的 QuestDB。默认路径是: ```text -raw_return = quantile(low[t+1:t+6], 0.20) / anchor_close - 1 -target = raw_return / asset_atr_price_top20_median +/home/ctyun/ban/banbot +/home/ctyun/ban/banbot/biz/config.yml ``` -这里的 20% 分位数是未来窗口的稳健低点参考,不会被单根异常影线完全决定;它不是严格的 `lowestLow`。负值表示该低点参考低于当前收盘价,正值表示它仍高于当前收盘价;因此上涨阶段不会被强行压成 0。`asset_atr_price_top20_median` 是该品种训练期 ATR/price 降序前 20% 的中位数,预测输出会乘回该尺度。训练锚点和未来 6 根必须连续,跨越测试边界的训练样本会被丢弃,避免把测试期标签泄漏进训练集。 - -## 运行 - -先安装依赖: +安装项目及依赖: ```bash python3 -m venv .venv .venv/bin/pip install -e . ``` -使用 banbot 的配置连接数据库,筛选 `market=linear`、符号为 `BASE/USDT:USDT`、上市至少 730 天且未退市的品种;默认连接配置来自 `/home/ctyun/ban/banbot/biz/config.yml`: +一键更新最新 K 线、导出完整 8H 数据并生成全部品种预测: ```bash +./scripts/update_and_predict.sh +``` + +结果默认写入: + +```text +predictions/latest_v1/predictions.csv +predictions/latest_v1/report.json +``` + +查看脚本参数: + +```bash +./scripts/update_and_predict.sh --help +``` + +完整的数据流、参数和目录说明见 [`docs/usage.md`](docs/usage.md)。 + +## 手动流程 + +如果需要单独准备数据和训练模型: + +```bash +# 下载 1H K 线,导出并整理为 8H CSV .venv/bin/python -m cbpred prepare \ --banbot-config /home/ctyun/ban/banbot/biz/config.yml \ --out-dir data -``` -如果数据已经由 banbot 下载完成,可以跳过下载,只执行筛选和导出: - -```bash -.venv/bin/python -m cbpred prepare --skip-download --out-dir data -``` - -训练会执行三轮特征迭代。点模型候选包括随机森林平方误差和 ExtraTrees,另用 HistGradientBoosting 分位数模型输出 q20/q80;验证集和测试集均为最近 10 个月中的按品种随机拆分,测试集只在最终选模后使用: - -```bash +# 使用 data/ 训练并覆盖 artifacts/v1 下的正式模型和报告 .venv/bin/python -m cbpred train \ --data-dir data \ --artifact-dir artifacts/v1 + +# 对单个已导出的 8H CSV 使用最新锚点预测 +.venv/bin/python -m cbpred predict \ + --artifact artifacts/v1/model.joblib \ + --csv data/raw/123_BTC_USDT_USDT.csv \ + --symbol BTC/USDT:USDT ``` -默认保留所有连续样本,把当前 ATR 在过去 200 根 8H K 线中的排名作为特征;可以通过 `--volatility-rank-threshold 0.80` 或 `0.90` 做高波动样本对照实验。训练阶段会把数据集行数、过滤前后数量、阶段和候选模型进度输出到 stderr。 - -一条命令完成准备和训练: +也可以用 `run` 完成 `prepare + train`: ```bash .venv/bin/python -m cbpred run --out-dir data --artifact-dir artifacts/v1 ``` -输出在 `artifacts/v1/report.md`、`artifacts/v1/report.json` 和 `artifacts/v1/model.joblib`。当前 `v1` 是最终特征复核中保留的 `compact6` 模型。 +如果数据已经由 banbot 更新完成,可在 `prepare` 中加入 `--skip-download`,只执行筛选、导出和整理。 -对某个已导出的 8H CSV 预测任意锚点: +## 预测 CSV -```bash -.venv/bin/python -m cbpred predict \ - --artifact artifacts/v1/model.joblib \ - --csv data/raw/123_BTC_USDT_USDT.csv \ - --at 2026010100000000 -``` +一键脚本生成的 CSV 固定只包含以下 10 列: -`future_low_return_q20` 是稳健未来低点收益的较低分位,对应 `future_low_price_q20` 这个保守的价格下界;`future_low_return_q80` 是较浅、较乐观的分位。负收益才表示回撤,正收益表示稳健未来低点仍高于当前收盘价。兼容字段 `drawdown_*` 等于对应收益取负,实际覆盖率应以测试集校准报告为准,不能把 80% 当成保证概率。 +| 列 | 含义 | +| --- | --- | +| `rank` | 按 8H USDT 成交额从高到低的排名 | +| `symbol` | 合约符号 | +| `latest_bar_start_utc` | 最新完整 8H K 线的 UTC 起始时间 | +| `close` | 最新完整 8H K 线收盘价 | +| `quote_volume_usdt_8h` | 最新 8H 的 USDT 成交额 | +| `atr_rank_200_pct` | 当前 ATR 在过去 200 根 8H ATR 中的百分位,单位为 `%` | +| `drawdown_point_pct` | 点预测对应的回撤百分比 | +| `drawdown_cap_80pct_pct` | q80 对应的回撤百分比上界参考 | +| `price_level_reached_80pct` | q20 对应的价格水平参考 | +| `price_level_cap_80pct` | q80 对应的价格水平参考 | -如果预测时 `eligible_for_model` 为 `false`,说明过去 200 根不足以计算 ATR 排名,或显式设置的 ATR 排名门槛未达到,返回的模型数值为 `null`,应跳过该时刻。 +回撤列是收益的相反数:预测收益为负时表示价格可能低于当前收盘;预测收益为正时,回撤列可能为负,表示未来稳健低点仍高于当前收盘。模型不满足资格条件时,预测字段为空,不应把空值当作零回撤。 -## 特征迭代 +## 当前模型 -第一轮使用 18 根 K 线的方向、实体/影线占比、收盘在 K 线中的位置、相对波幅、对数收益,以及基于法币成交额的 18 根窗口成交量排名和相对中位数;再加入两个固定的品种画像值:训练期 `ATR/price` 降序前 20% 的中位数、训练期法币成交额对数前 20% 的中位数。所有绝对价格都被转换为比例或滚动相对量,品种画像再用训练品种的均值/标准差标准化。 +当前模型为 `compact6` 特征版本,使用 146 个特征和 ExtraTrees 点模型,同时使用两个 HistGradientBoosting 分位数模型生成 q20/q80。测试集结果和特征定义见 [`docs/model.md`](docs/model.md),训练产物报告见 [`artifacts/v1/report.md`](artifacts/v1/report.md)。 -第二轮追加归一化 ATR、RSI、布林带位置/宽度、EMA 距离、趋势收益、滚动波动率和滚动回撤;第三轮追加 18 根窗口的趋势、波幅、方向比例、成交量趋势、收盘位置和指标交互摘要。三轮使用同一批训练/验证/测试边界,选择验证 q80 pinball loss 加覆盖率偏差后的最佳组合,最后只在测试集报告一次。 +## 测试 运行标准库回归测试: ```bash -.venv/bin/python -m unittest discover -v +.venv/bin/python -m unittest discover -q ``` + +项目文档按用途分为: + +- [`docs/usage.md`](docs/usage.md):安装、数据流、命令、目录和故障排查。 +- [`docs/model.md`](docs/model.md):目标定义、特征、训练协议、指标和限制。 +- [`docs/v0_0823.md`](docs/v0_0823.md)、[`docs/v1_0823.md`](docs/v1_0823.md):历史实验记录,不是当前运行入口。 diff --git a/cbpred/latest_predict.py b/cbpred/latest_predict.py new file mode 100644 index 0000000..dd6994c --- /dev/null +++ b/cbpred/latest_predict.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +import argparse +import csv +import json +import socket +import subprocess +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from urllib.parse import urlparse + +import pandas as pd + +from .pipeline import load_database_url, parse_time_ms, predict_from_csv, prepare_data + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_COLUMNS = ( + "rank", + "symbol", + "latest_bar_start_utc", + "close", + "quote_volume_usdt_8h", + "atr_rank_200_pct", + "drawdown_point_pct", + "drawdown_cap_80pct_pct", + "price_level_reached_80pct", + "price_level_cap_80pct", +) + + +def _utc_label(timestamp_ms: int) -> str: + return datetime.fromtimestamp(timestamp_ms / 1000, timezone.utc).strftime( + "%Y-%m-%d %H:%M:%S UTC" + ) + + +def _database_endpoint(dsn: str) -> tuple[str, int]: + parsed = urlparse(dsn) + return parsed.hostname or "127.0.0.1", parsed.port or 8812 + + +def ensure_database( + banbot_config: Path, + dsn: str | None, +) -> None: + database_url = load_database_url(banbot_config, dsn) + host, port = _database_endpoint(database_url) + try: + with socket.create_connection((host, port), timeout=1): + return + except OSError: + pass + if port != 8812: + return + + print("[latest-predict] QuestDB is unavailable; letting banbot start it", flush=True) + start_ms = int(datetime.now(timezone.utc).timestamp() * 1000) - 8 * 60 * 60 * 1000 + end_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + with tempfile.TemporaryDirectory(prefix="cbpred-db-bootstrap-") as output_dir: + subprocess.run( + [ + "go", + "run", + "./cmd/cbpred-export", + "--config", + str(banbot_config), + "--data-dir", + str(banbot_config.parent), + "--pairs", + "BTC/USDT:USDT", + "--start-ms", + str(start_ms), + "--end-ms", + str(end_ms), + "--out-dir", + output_dir, + ], + cwd=REPO_ROOT, + check=True, + ) + + +def build_predictions(data_dir: Path, artifact: Path, output_dir: Path) -> dict[str, object]: + manifest = json.loads((data_dir / "manifest.json").read_text()) + rows: list[dict[str, object]] = [] + total = len(manifest["symbols"]) + for index, item in enumerate(manifest["symbols"], 1): + csv_path = data_dir / item["path"] + frame = pd.read_csv(csv_path).sort_values("timestamp").drop_duplicates("timestamp") + latest = frame.iloc[-1] + result = predict_from_csv(artifact, csv_path, symbol=item["symbol"]) + eligible = bool(result["eligible_for_model"]) + rows.append( + { + "symbol": result["symbol"], + "latest_bar_start_utc": _utc_label(int(result["timestamp"])), + "close": result["close"], + "quote_volume_usdt_8h": float(latest["quote"]), + "atr_rank_200_pct": ( + result["volatility_rank_200"] * 100 + if result["volatility_rank_200"] is not None + else None + ), + "drawdown_point_pct": result["drawdown_point"] * 100 if eligible else None, + "drawdown_cap_80pct_pct": ( + result["drawdown_upper80_q80"] * 100 if eligible else None + ), + "price_level_reached_80pct": result["price_level_hit80"] if eligible else None, + "price_level_cap_80pct": result["price_level_upper80"] if eligible else None, + } + ) + if index == 1 or index == total or index % 25 == 0: + print(f"[latest-predict] predicted {index}/{total}", flush=True) + + rows.sort(key=lambda row: float(row["quote_volume_usdt_8h"]), reverse=True) + for rank, row in enumerate(rows, 1): + row["rank"] = rank + + output_dir.mkdir(parents=True, exist_ok=True) + output_csv = output_dir / "predictions.csv" + with output_csv.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=OUTPUT_COLUMNS) + writer.writeheader() + writer.writerows(rows) + + summary = { + "model": str(artifact.resolve()), + "symbols": len(rows), + "eligible_predictions": sum(row["drawdown_point_pct"] is not None for row in rows), + "latest_bar_start_utc": rows[0]["latest_bar_start_utc"] if rows else None, + "csv": str(output_csv.resolve()), + } + (output_dir / "report.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False)) + return summary + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Update banbot K-lines and predict with the current model") + parser.add_argument("--banbot-dir", type=Path, default=Path("/home/ctyun/ban/banbot")) + parser.add_argument( + "--banbot-config", + type=Path, + default=Path("/home/ctyun/ban/banbot/biz/config.yml"), + ) + parser.add_argument("--data-dir", type=Path, default=REPO_ROOT / "data") + parser.add_argument("--artifact", type=Path, default=REPO_ROOT / "artifacts/v1/model.joblib") + parser.add_argument("--output-dir", type=Path, default=REPO_ROOT / "predictions/latest_v1") + parser.add_argument("--history-months", type=int, default=36) + parser.add_argument("--holdout-months", type=int, default=10) + parser.add_argument("--min-age-days", type=int, default=730) + parser.add_argument("--chunk-size", type=int, default=100) + parser.add_argument("--end", default=None, help="UTC date/ISO timestamp or milliseconds") + parser.add_argument("--dsn", default=None) + parser.add_argument("--db-type", choices=["auto", "questdb", "timescale"], default="auto") + parser.add_argument("--exchange", default="binance") + return parser + + +def main() -> None: + args = build_parser().parse_args() + data_dir = args.data_dir.resolve() + artifact = args.artifact.resolve() + output_dir = args.output_dir.resolve() + ensure_database(args.banbot_config, args.dsn) + manifest = prepare_data( + out_dir=data_dir, + banbot_dir=args.banbot_dir, + banbot_config=args.banbot_config, + dsn=args.dsn, + db_type=args.db_type, + exchange=args.exchange, + end_ms=parse_time_ms(args.end) if args.end else None, + history_months=args.history_months, + holdout_months=args.holdout_months, + min_age_days=args.min_age_days, + chunk_size=args.chunk_size, + ) + print( + f"[latest-predict] exported {len(manifest['symbols'])} symbols; " + f"excluded {len(manifest['excluded'])}", + flush=True, + ) + summary = build_predictions(data_dir, artifact, output_dir) + print(json.dumps(summary, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/docs/model.md b/docs/model.md new file mode 100644 index 0000000..4bccb4a --- /dev/null +++ b/docs/model.md @@ -0,0 +1,125 @@ +# 模型与评估 + +本文描述当前默认产物 [`artifacts/v1/model.joblib`](../artifacts/v1/model.joblib),不是所有历史实验的合集。 + +## 当前模型 + +| 项目 | 当前值 | +| --- | --- | +| 模型版本 | `v1` 目录,序列化版本 `4` | +| 特征版本 | `compact6` | +| 时间周期 | 8H | +| 输入窗口 | 最近 18 根已收盘 8H K 线 | +| 预测窗口 | 后续 6 根 8H K 线 | +| 点模型 | `ExtraTreesRegressor`,平方误差 | +| 分位数模型 | 两个 `HistGradientBoostingRegressor`,q20/q80 | +| 特征数 | 146 | +| 当前 ATR 过滤阈值 | `0.0`,不主动过滤样本 | + +正式模型文件、训练报告和机器可读指标分别是: + +```text +artifacts/v1/model.joblib +artifacts/v1/report.md +artifacts/v1/report.json +``` + +## 目标定义 + +对时间点 `t`,先取未来 6 根 K 线的最低价,再取这些最低价的 20% 分位数作为稳健低点参考: + +```text +future_low_reference = quantile(low[t+1:t+6], 0.20) +raw_return = future_low_reference / close[t] - 1 +target = raw_return / asset_atr_price_top20_median +``` + +这不是单根最低影线的 `lowestLow`。使用 20% 分位数可以降低一根异常影线对标签的支配,同时保留上涨阶段未来低点高于当前收盘的正收益。负 `raw_return` 表示未来稳健低点低于当前收盘,正值表示它高于当前收盘。 + +`asset_atr_price_top20_median` 是该品种训练期 `ATR / close` 按降序取前 20% 后的中位数。模型在训练时预测归一化目标,输出时再乘回该品种尺度,使不同价格和波动水平的品种可以由同一个模型处理。 + +## 特征 + +当前 `compact6` 由 `18 × (6 + 2) + 2 = 146` 个特征组成: + +### 最近 18 根 K 线形态 + +每根 K 线保留 6 个无量纲或相对特征: + +- `body_range`:有符号实体相对 K 线振幅; +- `upper_wick`、`lower_wick`:上下影线相对振幅; +- `close_location`:收盘价在本根高低区间中的位置; +- `range_pct`:振幅相对收盘价; +- `log_return`:对数收益率。 + +没有把绝对价格直接输入模型,也没有在 `compact6` 中重复保存实体绝对大小或独立的阳阴线标记。 + +### 最近 18 根成交量 + +- `quote_volume_rank`:本根 USDT 成交额在最近 18 根中的百分位排名; +- `quote_volume_log_rel_median`:本根 USDT 成交额对最近 18 根中位数的对数相对值。 + +这里使用 `quote` 成交额,而不是不同币种单位不可比的原始成交量。 + +### 品种画像 + +每个品种只保留两个训练期画像值,并在训练品种之间标准化: + +- 高波动 20% 样本的 `ATR / close` 中位数; +- 高成交额 20% 样本的 `log1p(quote)` 中位数。 + +RSI、SMA、布林带、EMA、趋势汇总、全局市场摘要和横截面排名都做过候选实验,但当前正式模型没有使用它们。全局和横截面特征还会改变预测所需的上下文,不能直接用于当前的单 CSV 预测接口。 + +## 训练协议 + +默认使用最近 36 个月数据,最后 10 个月作为留出期。训练快照以前的样本只能使用完整的历史窗口和未来 6 根 K 线;跨越测试边界或数据缺口的样本会被丢弃。 + +品种先按固定随机种子 `42` 拆分,约 60% 作为训练品种、20% 作为验证品种、20% 作为测试品种。最近 10 个月再按时间拆成: + +- 验证品种的前 30%:模型和特征选择; +- 验证品种的接续 20%:分位数校准; +- 测试品种的最后 50%:最终测试,只报告一次。 + +每个阶段会比较随机森林和 ExtraTrees 点模型,并使用 q20/q80 分位数模型。特征阶段和候选模型只根据验证选择分数确定,最终测试集不参与选择。 + +## 测试结果 + +当前报告的最终测试集包含 13,950 个样本。误差是在乘回品种 ATR 尺度后的原始收益空间中计算: + +| 指标 | `compact6` | +| --- | ---: | +| 点预测 MAE | `0.024918` | +| 点预测 RMSE | `0.033563` | +| q80 覆盖率 | `77.50%` | +| q20 下界命中率 | `91.60%` | +| q20 覆盖率(`y <= q20`) | `8.40%` | + +相同复核配置下,182 特征基线的点预测 MAE/RMSE 是 `0.025140 / 0.033789`,`compact6` 分别改善约 `0.88% / 0.67%`。q80 的实测覆盖率是测试集统计结果,不是未来每次预测的保证概率;`80%` 也不能被理解为盈利概率或交易成功率。 + +q20 的两个指标不要混淆:`q20_coverage` 统计真实目标低于 q20 预测的比例,`q20_hit_rate` 统计真实目标高于 q20 预测的比例,所以二者在没有相等值时大致互补。 + +## 预测字段语义 + +模型内部预测的是 signed future-low return,批量 CSV 将其转成更直观的字段: + +```text +drawdown = -future_low_return × 100 +price_level = close × (1 + future_low_return) +``` + +因此: + +- `drawdown_point_pct` 是点预测; +- `drawdown_cap_80pct_pct` 使用 q80 收益,对应较浅的回撤上界参考; +- `price_level_reached_80pct` 使用 q20 收益,对应较保守的较低价格参考; +- `price_level_cap_80pct` 使用 q80 收益,对应较高的价格参考。 + +当未来稳健低点预测为正收益时,回撤列可以为负,这表示模型没有把上涨阶段强行截断为零。 + +## 使用限制 + +- 这是离线历史数据上的统一回归模型,不是交易信号,也不保证未来表现。 +- 测试结果依赖当前数据快照、品种筛选和时间切分;更新数据后重新训练,指标可能变化。 +- 模型只对保存了训练画像且有足够历史的品种预测。当前 ATR 排名至少需要 200 根 8H K 线。 +- `atr_rank_200_pct` 是状态描述,不代表模型只预测高波动时刻;正式阈值为 `0.0`。 +- 预测结果中的空值表示当前时刻不满足模型资格条件,不应替换成零。 diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..ae317cf --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,134 @@ +# 使用与数据流 + +本文是当前代码的运行说明。历史特征实验不在这里维护;它们只用于追溯模型选择过程。 + +## 依赖 + +运行需要: + +- Python 3 和本项目的虚拟环境; +- `pandas`、`numpy`、`scikit-learn`、`joblib` 等项目依赖; +- Go 和 `/home/ctyun/ban/banbot` 源码; +- banbot 配置中的 QuestDB 数据库,以及对 Binance 数据的访问。 + +默认配置文件是 `/home/ctyun/ban/banbot/biz/config.yml`。如果路径不同,可通过 `--banbot-config` 和 `--banbot-dir` 覆盖。也可以直接传入 `--dsn`。 + +安装: + +```bash +python3 -m venv .venv +.venv/bin/pip install -e . +``` + +## 一键更新与预测 + +```bash +./scripts/update_and_predict.sh +``` + +脚本调用 `python -m cbpred.latest_predict`,按以下顺序执行: + +1. 检查配置中的 QuestDB 是否可连接;QuestDB 默认不可用时,通过 banbot 的导出命令触发其启动流程。 +2. 从数据库筛选 Binance `linear` 市场、结算币为 USDT、上市至少 730 天且未退市的品种。 +3. 调用 banbot 下载最近 36 个月的 1H K 线。 +4. 调用 banbot 导出原始 K 线,并只保留连续完整的 8 根 1H K 线聚合成 8H K 线。 +5. 写入 `data/manifest.json` 和 `data/raw/*.csv`。 +6. 加载 `artifacts/v1/model.joblib`,对清单中的每个品种使用最新完整 8H K 线预测。 +7. 按最新 8H USDT 成交额降序写入 `predictions/latest_v1/predictions.csv`,并写入 `report.json`。 + +脚本的默认参数可以覆盖: + +```bash +./scripts/update_and_predict.sh \ + --banbot-dir /path/to/banbot \ + --banbot-config /path/to/config.yml \ + --data-dir data \ + --artifact artifacts/v1/model.joblib \ + --output-dir predictions/latest_v1 \ + --end 2026-08-23T00:00:00Z +``` + +`--end` 用于固定数据快照,支持 UTC 日期、ISO 时间或毫秒时间戳。默认使用当前 UTC 时间向下取整到 8H 边界。 + +## 手动命令 + +### 准备数据 + +```bash +.venv/bin/python -m cbpred prepare \ + --banbot-config /home/ctyun/ban/banbot/biz/config.yml \ + --out-dir data +``` + +只使用已经下载的数据: + +```bash +.venv/bin/python -m cbpred prepare \ + --banbot-config /home/ctyun/ban/banbot/biz/config.yml \ + --out-dir data \ + --skip-download +``` + +`prepare` 的默认范围是最近 36 个月,训练快照前的品种至少上市 730 天,最近 10 个月预留给测试。`manifest.json` 记录筛选出的品种、数据范围、数据库类型和被排除的原因。 + +### 训练模型 + +```bash +.venv/bin/python -m cbpred train \ + --data-dir data \ + --artifact-dir artifacts/v1 +``` + +训练会输出阶段和候选模型进度到 stderr,并在 `artifacts/v1/` 写入: + +- `model.joblib`:当前可供预测的模型; +- `report.json`:机器可读的训练协议和评估指标; +- `report.md`:人类可读的评估摘要。 + +`--volatility-rank-threshold` 可以做 ATR 排名过滤实验。正式 `v1` 模型的阈值是 `0.0`,因此不会过滤低波动样本;`atr_rank_200_pct` 仍会作为当前状态字段输出。 + +### 单品种预测 + +```bash +.venv/bin/python -m cbpred predict \ + --artifact artifacts/v1/model.joblib \ + --csv data/raw/123_BTC_USDT_USDT.csv \ + --symbol BTC/USDT:USDT +``` + +不传 `--at` 时使用 CSV 中最新完整 8H K 线。传入 `--at` 时,时间必须精确匹配一个 8H K 线起始时间,且不能早于模型训练快照。 + +模型需要至少 200 根 8H K 线计算 ATR 排名,并需要模型中保存的该品种训练画像。`eligible_for_model` 为 `false` 时,预测数值返回空值,并包含 `skip_reason`。 + +## 文件布局 + +```text +cbpred/ +├── cbpred/ # Python CLI、特征和训练/预测管线 +├── scripts/update_and_predict.sh # 一键入口 +├── data/ +│ ├── manifest.json # 数据快照和品种清单 +│ └── raw/*.csv # 导出的 8H OHLCV +├── artifacts/ +│ └── v1/ # 唯一正式模型版本 +└── predictions/ + └── latest_v1/ # 最近一次批量预测 +``` + +模型版本目录使用 `v1` 形式维护。历史实验可以保留在文档或实验输出目录中,但不应作为默认预测模型或在项目根目录新增多个 `artifacts_*` 入口。 + +## 故障排查 + +- `missing virtualenv Python`:先执行安装命令,确认 `.venv/bin/python` 存在。 +- QuestDB 连接失败:检查 banbot 配置中的数据库地址、QuestDB 服务和端口;一键脚本只对默认 QuestDB 端口尝试触发 banbot 启动。 +- `missing data/manifest.json`:先运行 `prepare`,或确认一键脚本没有在下载/导出阶段失败。 +- 某品种预测为空:检查 `eligible_for_model` 和 `skip_reason`,通常是 200 根 ATR 历史不足或该品种没有正式模型画像。 +- 数据只有部分 8H K 线:导出会丢弃不连续或不完整的 8 根 1H 桶,这是为了避免特征和未来标签跨越缺口。 + +## 验证 + +```bash +./scripts/update_and_predict.sh --help +.venv/bin/python -m unittest discover -q +git diff --check +``` diff --git a/docs/v0_0823.md b/docs/v0_0823.md index 6709755..787a29c 100644 --- a/docs/v0_0823.md +++ b/docs/v0_0823.md @@ -1,4 +1,6 @@ +> 历史实验记录:本文保留 v0 阶段的特征分析。当前运行入口、正式模型和最新指标以 [`README.md`](../README.md)、[`usage.md`](usage.md) 和 [`model.md`](model.md) 为准。 + 特征 维度 是否启用 介绍 ───────────────────────────── ─────────── ────────────── ───────────────────────────────────────── 最近18个K线形态特征 144 是 @@ -73,4 +75,4 @@ - 品种固定画像:约 0.7% 这说明模型主要依赖最近18根K线的形态和波动结构,成交量有辅助作用,长期品种画像目前影响较小。这个重要性 -只代表 ExtraTrees 点模型的特征分裂贡献,不等同于因果关系。 \ No newline at end of file +只代表 ExtraTrees 点模型的特征分裂贡献,不等同于因果关系。 diff --git a/docs/v1_0823.md b/docs/v1_0823.md index 65772b3..ea6ed6c 100644 --- a/docs/v1_0823.md +++ b/docs/v1_0823.md @@ -1,3 +1,5 @@ +> 历史实验记录:本文保留 v1 特征复核过程。当前运行入口、正式模型和最新指标以 [`README.md`](../README.md)、[`usage.md`](usage.md) 和 [`model.md`](model.md) 为准。 + 我考虑下进行下面的尝试: 1. 每个K线提取8个信息,其中冗余的不少,尝试压缩下;只要下降不是很多就可以保留。 diff --git a/scripts/update_and_predict.sh b/scripts/update_and_predict.sh new file mode 100755 index 0000000..a152786 --- /dev/null +++ b/scripts/update_and_predict.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_BIN="$REPO_ROOT/.venv/bin/python" + +if [[ ! -x "$PYTHON_BIN" ]]; then + printf 'missing virtualenv Python: %s\n' "$PYTHON_BIN" >&2 + exit 1 +fi + +cd "$REPO_ROOT" +exec "$PYTHON_BIN" -m cbpred.latest_predict "$@" diff --git a/tests/test_latest_predict.py b/tests/test_latest_predict.py new file mode 100644 index 0000000..47a5946 --- /dev/null +++ b/tests/test_latest_predict.py @@ -0,0 +1,26 @@ +import unittest + +from cbpred.latest_predict import OUTPUT_COLUMNS + + +class LatestPredictTests(unittest.TestCase): + def test_output_columns_are_stable(self): + self.assertEqual( + OUTPUT_COLUMNS, + ( + "rank", + "symbol", + "latest_bar_start_utc", + "close", + "quote_volume_usdt_8h", + "atr_rank_200_pct", + "drawdown_point_pct", + "drawdown_cap_80pct_pct", + "price_level_reached_80pct", + "price_level_cap_80pct", + ), + ) + + +if __name__ == "__main__": + unittest.main()