支持一键预测脚本
This commit is contained in:
105
README.md
105
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):历史实验记录,不是当前运行入口。
|
||||
|
||||
190
cbpred/latest_predict.py
Normal file
190
cbpred/latest_predict.py
Normal 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
125
docs/model.md
Normal 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
134
docs/usage.md
Normal 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
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
> 历史实验记录:本文保留 v0 阶段的特征分析。当前运行入口、正式模型和最新指标以 [`README.md`](../README.md)、[`usage.md`](usage.md) 和 [`model.md`](model.md) 为准。
|
||||
|
||||
特征 维度 是否启用 介绍
|
||||
───────────────────────────── ─────────── ────────────── ─────────────────────────────────────────
|
||||
最近18个K线形态特征 144 是
|
||||
|
||||
@@ -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
13
scripts/update_and_predict.sh
Executable 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 "$@"
|
||||
26
tests/test_latest_predict.py
Normal file
26
tests/test_latest_predict.py
Normal 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()
|
||||
Reference in New Issue
Block a user