支持一键预测脚本

This commit is contained in:
2026-08-23 14:30:20 +08:00
parent dc65b98568
commit 286626dfe5
8 changed files with 561 additions and 38 deletions

105
README.md
View File

@@ -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):历史实验记录,不是当前运行入口。

190
cbpred/latest_predict.py Normal file
View File

@@ -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()

125
docs/model.md Normal file
View File

@@ -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`。
- 预测结果中的空值表示当前时刻不满足模型资格条件,不应替换成零。

134
docs/usage.md Normal file
View File

@@ -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
```

View File

@@ -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 点模型的特征分裂贡献,不等同于因果关系。
只代表 ExtraTrees 点模型的特征分裂贡献,不等同于因果关系。

View File

@@ -1,3 +1,5 @@
> 历史实验记录:本文保留 v1 特征复核过程。当前运行入口、正式模型和最新指标以 [`README.md`](../README.md)、[`usage.md`](usage.md) 和 [`model.md`](model.md) 为准。
我考虑下进行下面的尝试:
1. 每个K线提取8个信息,其中冗余的不少,尝试压缩下;只要下降不是很多就可以保留。

13
scripts/update_and_predict.sh Executable file
View File

@@ -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 "$@"

View File

@@ -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()