Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8bf8fddb | ||
|
|
0f6eb92a1f | ||
|
|
9880e236a5 | ||
|
|
ec08de098e | ||
|
|
6c627f009a | ||
|
|
dbb6202325 | ||
|
|
efad2bb333 | ||
|
|
2964d6f230 | ||
|
|
7991a6b2bf | ||
|
|
276481e02c |
+35
-34
@@ -1,48 +1,49 @@
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
# Python编译文件和缓存
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
|
||||
# Logs & databases
|
||||
# 策略文件的缓存
|
||||
strategies/__pycache__/
|
||||
|
||||
# Machine Learning / AI model files
|
||||
*_model*_xgb_model.json
|
||||
*modelchan*.json
|
||||
*.libsvm
|
||||
feature_meta
|
||||
*_model_feature_data.csv
|
||||
*.pem
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Database files
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
|
||||
# Office documents kept alongside the repo but not part of it.
|
||||
# "~$" files are Excel's lock files, recreated every time a workbook is opened.
|
||||
*.xlsx
|
||||
*.xls
|
||||
~$*
|
||||
|
||||
# Local data
|
||||
data/
|
||||
|
||||
# Exchange history fetched by research/live/*.py. 40MB and re-fetchable from
|
||||
# the venue, so it stays local; the small result CSVs it feeds are committed.
|
||||
research/live/cache/
|
||||
|
||||
# Scratch outputs from short shakedown runs, superseded by the real collection.
|
||||
research/out/archive/
|
||||
|
||||
# Per-trade simulation dumps from research/step*.py. 70MB+ and regenerable by
|
||||
# rerunning the step; the summaries they feed live in HANDOFF.md.
|
||||
research/out/*.feather
|
||||
|
||||
# Virtualenvs. venv writes its own .gitignore since 3.11, but only for the
|
||||
# directory it creates — declare it here so other layouts are covered too.
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Local tooling
|
||||
.DS_Store
|
||||
交易记录/~$交易规则.docx
|
||||
/datasvc/data
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
/data_provider/data
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
data_provider/._config.json
|
||||
.gstack/
|
||||
research/out/*.jsonl.gz
|
||||
research/out/penetration.csv
|
||||
research/out/shadow_*.csv
|
||||
research/out/run_meta_*.json
|
||||
|
||||
# ESS gate / engineering-loop working dirs(归档进 docs/runs/)
|
||||
.gates/
|
||||
loop/
|
||||
|
||||
# Crypto Wyckoff Screener local cache
|
||||
data/crypto_wyckoff/
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# chan — Agent Entry
|
||||
|
||||
本仓受 ESS 约束。不要一上来扫全库或加载全部 governance。
|
||||
|
||||
## Boot
|
||||
|
||||
1. `docs/PROJECT_PROFILE.md`
|
||||
2. `docs/PROJECT_RULES.md`
|
||||
3. `docs/STATE/CURRENT.md` + `docs/AGENT_MEMORY.md`
|
||||
4. 有进行中任务再读 `docs/TASKS/` / 对应 ECR / HANDOFF
|
||||
5. 角色文件:ESS 根目录 `agents/{ARCHITECT|ENGINEER|REVIEWER|RELEASE_MANAGER}.md`
|
||||
|
||||
## Roles(选一)
|
||||
|
||||
| 意图 | 角色 |
|
||||
|------|------|
|
||||
| 规格 / 架构 / ECR | ARCHITECT |
|
||||
| 实现 / 修 bug | ENGINEER |
|
||||
| 审阅 | REVIEWER |
|
||||
| 发版 / tag | RELEASE_MANAGER |
|
||||
|
||||
## Never
|
||||
|
||||
- 无 ECR 改 `config/` / `strategies/` 交易逻辑
|
||||
- 无 ADR 改缠论算法语义
|
||||
- 无 ECR 删减 `/api/analyze` 字段
|
||||
- 把聊天记录当成完成;阶段结束须落盘 `docs/`
|
||||
|
||||
## Pointers
|
||||
|
||||
- TRACEABILITY: `docs/TRACEABILITY.md`
|
||||
- CHANGELOG: `docs/CHANGELOG/CHANGELOG.md`
|
||||
- 人类向导:`CLAUDE.md`
|
||||
@@ -0,0 +1,116 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
缠论 (Chan Theory) technical analysis system for Freqtrade. Implements Chan Zhong Shui Chan's theory for crypto/stock trading, including fractal (分型), stroke (笔), segment (线段), pivot/center (中枢), and buy/sell point (买卖点) detection.
|
||||
|
||||
## Governance
|
||||
|
||||
- Agent 入口:`AGENTS.md`(boot 顺序)· `docs/PROJECT_PROFILE.md` · `docs/AGENT_MEMORY.md` · `docs/STATE/CURRENT.md`
|
||||
- ESS 文档:`docs/ECR/`、`docs/ENGINEERING_SPEC/`、`docs/TRACEABILITY.md`、`docs/CHANGELOG/`
|
||||
- **正式引擎包**:`chanlun/`;strategies / web 已用 `from chanlun import ...`
|
||||
- 根目录 `Chan*.py` / `TF_DF.py` 仍为 **兼容 shim**(旧脚本可用)
|
||||
- 变更分级:无 ECR 不改 strategies/config;无 ADR 不改缠论算法语义
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Chan Theory Engine (`chanlun/`)
|
||||
|
||||
```text
|
||||
chanlun/
|
||||
core/ # KLU KLC BI SBI SEG ZS BIZS BSP Enum CTime
|
||||
pipeline/ # orchestrator(ChanLun) + timeframe(TF_DF) + builders/
|
||||
indicators/ # ChanMACD*
|
||||
analysis/ # Zone Classifier Pivot Heng PY Find_Trend ...
|
||||
```
|
||||
|
||||
Data processing pipeline (each step feeds the next):
|
||||
|
||||
1. **`chanlun.core.ChanKLU`** — Raw K-line unit with TA indicators and pattern recognition
|
||||
2. **`chanlun.core.ChanKLC`** — Combined K-line: inclusion + fractal; `.next`/`.pre` linked list
|
||||
3. **`chanlun.core.ChanBI`** — Stroke (笔)
|
||||
4. **`chanlun.core.ChanSBI`** — Special stroke → SEG
|
||||
5. **`chanlun.core.ChanSEG`** — Segment (线段)
|
||||
6. **`chanlun.core.ChanZS`** / **`ChanBIZS`** — Centers (中枢)
|
||||
7. **`chanlun.core.ChanBSP`** — Buy/Sell points
|
||||
8. **`chanlun.pipeline.orchestrator.ChanLun`** — Orchestrator
|
||||
9. **`chanlun.pipeline.timeframe.TF_DF`** — Timeframe facade;实现拆在 `pipeline/builders/`
|
||||
|
||||
### Services
|
||||
|
||||
- **外部 DATA_SERVICE** — 行情服务(env: `DATA_SERVICE_URL`);本仓库可不含 data_provider 源码
|
||||
- **`web/`** — Flask UI:`create_app()` + `api/` blueprints + `services/`;前端 `static/js/app/`。默认端口见 `web/config.py`(`FLASK_PORT`,常见 8128)
|
||||
- **`strategies/`** — Freqtrade strategies(本 ECR 不改)
|
||||
- **`config/`** — Freqtrade configs(本 ECR 不改)
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Exchange / DATA_SERVICE → Freqtrade Strategy / web → ChanLun → TF_DF
|
||||
→ KLU → KLC → BI → SBI → SEG → ZS → BSP
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Freqtrade Trading
|
||||
|
||||
```bash
|
||||
# Live trade
|
||||
freqtrade trade -c ./user_data/Chan/config/<config>.json --strategy <StrategyName> --strategy-path ./user_data/Chan/strategies
|
||||
|
||||
# Backtest
|
||||
freqtrade backtesting -c ./user_data/Chan/config/<config>.json --strategy <StrategyName> --strategy-path ./user_data/Chan/strategies --timerange=20251008-
|
||||
|
||||
# Download data
|
||||
freqtrade download-data -c ./user_data/Chan/config/<config>.json -t 1m 1h 1d --pairs BTC/USDT:USDT --timerange=20240101-
|
||||
|
||||
# Hyperopt
|
||||
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy <StrategyName> --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/<config>.json -e 200 --timerange=20250201-20250901
|
||||
|
||||
# Plot
|
||||
freqtrade plot-dataframe --strategy <StrategyName> --datadir user_data/data/binance -c ./user_data/Chan/config/<config>.json --timerange=20250721-
|
||||
```
|
||||
|
||||
### Data Provider
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
cd data_provider && docker compose up -d
|
||||
|
||||
# Direct
|
||||
cd data_provider && python main.py
|
||||
|
||||
# With custom config
|
||||
CONFIG_PATH=./config.json python main.py
|
||||
```
|
||||
|
||||
### Web UI
|
||||
|
||||
```bash
|
||||
cd web && python app.py
|
||||
# or via gunicorn:
|
||||
gunicorn -w 4 -b 0.0.0.0:8123 app:app
|
||||
|
||||
# Deploy scripts:
|
||||
cd web && ./deploy.sh # standard
|
||||
cd web && ./deploy_venv.sh # Ubuntu 22.04+ (venv)
|
||||
```
|
||||
|
||||
### Docker (Freqtrade)
|
||||
|
||||
```bash
|
||||
sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/<config>.json --strategy <StrategyName> --strategy-path ./user_data/Chan/strategies --timerange=20250721-
|
||||
```
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- All Chan theory classes are prefixed with `Chan` (e.g., `ChanBI`, `ChanZS`)
|
||||
- Strategies import `ChanLun` and add `sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))` to import from parent
|
||||
- MACD params: `MACD(26, 52, 9)` by default (slow period 52 instead of standard 26)
|
||||
- Enums in `ChanEnum.py` use `auto()` values
|
||||
- `ChanKLC` is a linked-list style data structure with `.next`/`.pre` pointers
|
||||
- The `TF_DF` class is the primary data container per timeframe
|
||||
- K-line direction uses `Chan_KLINE_DIR` (UP/DOWN/COMBINE/INCLUDED)
|
||||
- All text comments/commits are in Chinese
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanBI import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanBIZS import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanBSP import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanCTime import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanEnum import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanHeng import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanKLC import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanKLU import * # noqa: F403
|
||||
@@ -0,0 +1,3 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.pipeline.orchestrator import ChanLun # noqa: F401
|
||||
from chanlun.pipeline.timeframe import TF_DF # noqa: F401
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanLun_Classifier import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.indicators.ChanMACD import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.indicators.ChanMACDHistSet import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.indicators.ChanMACDSeg import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.indicators.ChanMACDUnitTF import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanPY import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanPivotClassifier import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanPivotMonitor import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanSBI import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanSEG import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.ChanZS import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.ChanZone import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.core.Chan_FX_Box import * # noqa: F403
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.analysis.Find_Trend import * # noqa: F403
|
||||
@@ -1,130 +0,0 @@
|
||||
# Chan — 缠论分析引擎
|
||||
|
||||
把 OHLCV K 线拆成缠论结构(K 线单元 → 合并 K 线 → 分型 → 笔 → 线段 → 中枢 → 买卖点),
|
||||
配一个 TradingView Charting Library 的 Web 界面,外加一套验证信号有效性的回测脚本。
|
||||
|
||||
标的不限:加密永续(ccxt)与 A 股(akshare)都走同一条分析链路。
|
||||
|
||||
```
|
||||
chanlun/ 缠论引擎,纯 pandas/numpy,无外部指标库依赖
|
||||
web/ Flask API + 图表界面
|
||||
research/ 信号有效性验证脚本(step1 ~ step30)
|
||||
data/ 本地 K 线(freqtrade 的 feather 格式)
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
需要 Python ≥ 3.11(pandas 3.x / numpy 2.x 的要求,不是本项目代码的限制)。
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt # 核心运行时,8 个包
|
||||
.venv/bin/pip install -r requirements-dev.txt # 另加测试与 research/ 所需
|
||||
```
|
||||
|
||||
## 跑 Web
|
||||
|
||||
```bash
|
||||
cd web
|
||||
../.venv/bin/python app.py # 默认 http://0.0.0.0:8128
|
||||
```
|
||||
|
||||
从仓库根跑 `.venv/bin/python web/app.py` 也可以——Python 会把脚本所在目录放进 `sys.path`。
|
||||
但**不能用 `python -m web.app`**,也不能 `import web.app`:`web/` 内部是无前缀导入
|
||||
(`import config`、`from api.analyze import bp`),`-m` 方式下 `sys.path` 里是仓库根而不是
|
||||
`web/`,会 `ModuleNotFoundError: No module named 'config'`。
|
||||
|
||||
配置全部走环境变量,见 `web/config.py`:
|
||||
|
||||
| 变量 | 默认值 | 用途 |
|
||||
|------|--------|------|
|
||||
| `FLASK_HOST` / `FLASK_PORT` | `0.0.0.0` / `8128` | 监听地址 |
|
||||
| `DATA_SERVICE_URL` | `https://provider.jackyu66.com` | 行情 REST 源 |
|
||||
| `DATA_SERVICE_WS_URL` | `wss://jackyu66.com/ws` | 行情 WebSocket 源 |
|
||||
| `ASHARE_DP_URL` | `http://103.179.242.166:8000` | A 股数据源 |
|
||||
| `CHAN_HTTP_PROXY` | 未设置则不走代理 | ccxt / HTTP 代理 |
|
||||
| `MACD_FACTOR` / `MACD_SMOOTH` | `1` / `1` | MACD 周期倍数,默认 12/26/9 |
|
||||
|
||||
主要接口:`GET /api/analyze` 返回某标的某周期的完整缠论结构,`/api/klines/recent`
|
||||
取最新 K 线,`/api/trend_filter` 与 `/api/trend_detail` 做多周期趋势筛选,
|
||||
`/api/symbols`、`/api/search_stock`、`/api/sectors` 等负责标的检索。页面在 `/` 与 `/chan_tv`。
|
||||
|
||||
## 作为库使用
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
from chanlun import TF_DF
|
||||
|
||||
# 需要 date/open/high/low/close/volume 六列,date 为 datetime
|
||||
df = pd.read_feather("data/binance/futures/BTC_USDT_USDT-1h-futures.feather")
|
||||
|
||||
tf = TF_DF(df, interval=1, timeframe="1h")
|
||||
|
||||
len(tf.klu_list) # K 线单元
|
||||
len(tf.klc_list) # 合并 K 线(处理包含关系后)
|
||||
len(tf.bi_list) # 笔
|
||||
len(tf.seg_list) # 线段
|
||||
len(tf.zs_list) # 中枢
|
||||
tf.bi_list[-1].dir # Chan_BI_DIR.DOWN
|
||||
tf.chanmacd # MACD 结构分析(背驰判定用)
|
||||
```
|
||||
|
||||
**`interval` 的单位是分钟**,对传入的 df 做重采样;`interval=1` 是特例,表示原样使用、
|
||||
不重采样。所以拿 1h 的 feather 要传 `interval=1`,拿 1m 数据想看 1h 才传 `interval=60`:
|
||||
|
||||
```python
|
||||
df1m = pd.read_feather("data/binance/futures/BTC_USDT_USDT-1m-futures.feather")
|
||||
TF_DF(df1m, interval=5, timeframe="5m")
|
||||
TF_DF(df1m, interval=60, timeframe="1h")
|
||||
```
|
||||
|
||||
传错不会报错,只会静默给出错误周期的结构——1h 数据配 `interval=4` 相当于按 4 分钟
|
||||
重采样,结果与 `interval=1` 完全相同。
|
||||
|
||||
## 分析流程
|
||||
|
||||
`TF_DF.init_TF_DF()` 按顺序做这几步,每步的实现在 `chanlun/pipeline/builders/` 下同名文件:
|
||||
|
||||
1. `resample_to_interval` — 重采样(`interval != 1` 时)
|
||||
2. `add_indicators` — 追加 33 列指标(MACD / BBANDS / EMA / RSI / ATR 等)
|
||||
3. `cal_kl_data` → `klu_list` — K 线单元
|
||||
4. `get_klc_list` → `klc_list` — 按包含关系合并 K 线,并标记分型
|
||||
5. `cal_bi_list` → `bi_list` — 笔
|
||||
6. `cal_bi_zs_list_pure` → `bi_zs_list` — 笔中枢
|
||||
7. `get_seg_list` → `seg_list` — 线段
|
||||
8. `get_zs_list` / `get_big_zs_list` — 中枢与大级别中枢
|
||||
9. `ChanMACD(klu_list)` — MACD 段 / 柱堆结构,供背驰判定
|
||||
|
||||
## 数据
|
||||
|
||||
`data/<交易所>/futures/<SYMBOL>-<周期>-futures.feather`,即 freqtrade 的下载格式,
|
||||
如 `data/binance/futures/BTC_USDT_USDT-1h-futures.feather`。
|
||||
`research/lib/data.py` 负责定位:`BTC/USDT:USDT` + `1h` 会解析到上面这个路径,
|
||||
找不到本地文件则回落到远端拉取。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest chanlun/tests web/tests -q
|
||||
```
|
||||
|
||||
`chanlun/tests/test_ta_compat.py` 有个**需要注意的陷阱**:它把 `chanlun/indicators/ta.py`
|
||||
的输出逐 bar 钉在 TA-Lib 上,但 **TA-Lib 不存在时会静默跳过**。也就是说改了 `ta.py`
|
||||
之后在没装 TA-Lib 的环境里跑,测试会显示通过,其实一项都没验证。改动那个文件时请先装:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libta-lib0 ta-lib-dev
|
||||
.venv/bin/pip install TA-Lib technical
|
||||
```
|
||||
|
||||
## 已知问题
|
||||
|
||||
- **`web/DEPLOY_GUIDE.md` 已失效**:它引用的 `deploy_venv.sh`、`stop_venv.sh`、
|
||||
`status_venv.sh` 等 6 个脚本都在 `7f393b9` 精简提交里删掉了,目前没有部署脚本。
|
||||
两个 systemd unit 文件(`web/chanlun-web*.service`)仍可参考,但它们用 gunicorn
|
||||
且写死端口 8123,与 `config.py` 默认的 8128 不一致,gunicorn 也不在依赖清单里。
|
||||
- **`web/README.txt` 已过时**:它说的 `web/requirements.txt` 不存在,依赖清单在仓库根目录。
|
||||
- **四个零引用的死文件**:`chanlun/analysis/` 下的 `ChanPY.py`、`ChanLun_Classifier.py`、
|
||||
`Find_Trend.py`、`ChanHeng.py` 全项目无人引用。`ChanPY.py` 依赖未安装的外部 chan.py 库,
|
||||
另外三个需要 matplotlib / mplfinance / xgboost / scikit-learn——这些都**不在**依赖清单里,
|
||||
是有意为之。要用得自行安装。
|
||||
@@ -0,0 +1,2 @@
|
||||
"""兼容 shim — 请优先 from chanlun import ..."""
|
||||
from chanlun.pipeline.timeframe import TF_DF # noqa: F401
|
||||
@@ -14,10 +14,12 @@ from chanlun.core.ChanSBI import ChanSBI
|
||||
from chanlun.core.ChanSEG import ChanSEG
|
||||
from chanlun.core.ChanZS import ChanZS
|
||||
from chanlun.core.ChanBSP import ChanBSP
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.dates import DateFormatter, date2num
|
||||
import matplotlib.patches as patches
|
||||
from technical.util import resample_to_interval
|
||||
from decimal import Decimal
|
||||
from chanlun.pipeline.orchestrator import ChanLun
|
||||
import xgboost as xgb
|
||||
|
||||
@@ -2,7 +2,7 @@ import ccxt
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import mplfinance as mpf
|
||||
from chanlun.indicators import ta
|
||||
from talib import MACD, SMA
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import datetime as dt
|
||||
@@ -249,9 +249,8 @@ def analyze_higher_timeframe(df_30m):
|
||||
# 8. Back-divergence detection (enhanced)
|
||||
def detect_back_divergence(df, strokes, higher_trend):
|
||||
try:
|
||||
macd_df = ta.MACD(df['Close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
macd, hist = macd_df['macd'], macd_df['macdhist']
|
||||
sma20 = ta.SMA(df['Close'], timeperiod=20)
|
||||
macd, signal, hist = MACD(df['Close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
sma20 = SMA(df['Close'], timeperiod=20)
|
||||
df['macd'] = macd
|
||||
df['hist'] = hist
|
||||
df['sma20'] = sma20
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
"""快速三类买卖点(引擎内称第四类,B4/S4)。
|
||||
|
||||
原本长在 `research/lib/fast_bsp3.py`,现移入引擎作为唯一实现,研究脚本改为转发导入,
|
||||
这样回测口径与 web 图表永远一致。
|
||||
|
||||
命名说明:缠论原文里没有「第四类买卖点」,但 B4/S4 也不只是「B3/S3 提前几根」——
|
||||
两者的选样口径不同,统计性质符号相反,故单独立类。形态条件是实时可判的:
|
||||
|
||||
中枢已成 -> 收盘突破 zg -> 收盘未跌回中枢 -> 重新上行
|
||||
|
||||
最后一步发生的当根就能下单,滞后约 2 根,而引擎 B3/S3 要等 pullback_bi.sure_time,
|
||||
滞后 9~10 根。但差别不止滞后:
|
||||
|
||||
判据 引擎用笔端点事后判(回拉笔低点 >= zg),本函数用收盘价实时判
|
||||
方向 引擎由离开笔方向决定,本函数由收盘从哪一侧突破决定
|
||||
口径 627 个中枢里引擎发 625 个信号(几乎不筛),本函数只认 212 个(34%);
|
||||
被拒的多数是「中枢确认时价格早已离开、此后再没回来」的历史区间
|
||||
|
||||
step30 同条件对拍(同一套 pure 笔中枢、同一组过滤器、同样的 1.5/3.0/48 出场):
|
||||
|
||||
原始 PF +大级别同向 +同向+阶梯 胜率 t值
|
||||
引擎 B3/S3 0.66 0.66 0.71 27.4% -18.76
|
||||
本函数 B4/S4 1.59 1.85 2.26 47.1% +10.09
|
||||
|
||||
同一组过滤器对 B4 有效、对 B3 无效,滞后差解释不了这一点(入场后移 1~4 根只是
|
||||
PF 3.18->2.53 的平滑衰减)。且 SL1.5/TP3.0 下随机入场胜率约 33%,引擎那 27.4%
|
||||
低于随机——它选中的是一批系统性反向的样本,不是「晚了所以差」。
|
||||
|
||||
require_touch 控制是否强求回抽碰到中枢边界。step32/33 的实测表明强求反而更差:
|
||||
这等于排除掉「突破后一去不回头」的强势段,而那正是缠论里最强的趋势形态。
|
||||
故默认 False(笔数 +21%、滞后 -1 根、PF 2.26→2.37)。
|
||||
|
||||
判据本身(收盘越过前一根极值)没有独立预测力:裸用 15 万笔样本 PF 0.95,
|
||||
把中枢换成「近20根高点」这类伪阻力位后 PF 0.90。alpha 全部来自中枢结构,
|
||||
判据只负责在这个已知价位上确认动能恢复。它也不能用于识别笔端点——
|
||||
入场前的回抽极值命中笔端点±2根的比例 19.3%,低于随机基准 22%。
|
||||
|
||||
全部判定只使用当根及之前的数据,无未来函数。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from chanlun.core.ChanEnum import Chan_FX_TYPE
|
||||
|
||||
# 中枢的「可用时刻」取第几笔的确认时间。
|
||||
# -1 = 最后一笔(历史默认)。中枢每吸收一根K线,最后一笔就可能后移,
|
||||
# 于是 available_ts 跟着漂——这是右边缘重画的根因(见 HANDOFF §5.41)。
|
||||
# 2 = 第三笔。三笔重叠即中枢成立,此后不再变,理论上更早也更稳。
|
||||
# ⛔ step47 已判定 bis[2] 作废(毛 R 0.933 → −0.000,见 HANDOFF §5.42)。
|
||||
# 开关保留只为可复现那次 A/B,**不要改默认值**。
|
||||
import os as _os
|
||||
|
||||
AVAIL_BI_INDEX = -1
|
||||
|
||||
|
||||
def _resolve_avail_bi(avail_bi: int | None) -> int:
|
||||
"""优先级:显式入参 > 环境变量 CHAN_AVAIL_BI > 模块默认。
|
||||
|
||||
环境变量在调用时读取而非 import 时——ProcessPoolExecutor 在 fork 启动方式下
|
||||
子进程会继承已 import 的模块,import 时读就固化成父进程的值了。
|
||||
"""
|
||||
if avail_bi is not None:
|
||||
return avail_bi
|
||||
return int(_os.environ.get("CHAN_AVAIL_BI", AVAIL_BI_INDEX))
|
||||
|
||||
|
||||
def timestamps_ms(src: pd.DataFrame) -> np.ndarray:
|
||||
"""取毫秒时间戳。研究侧的 df 自带 timestamp,web 侧的不一定,故按 date 回退。
|
||||
|
||||
回退写法不能用 `date.astype("int64") // 10**6`:该值单位取决于列精度,
|
||||
对毫秒精度的列会把时间戳砸平。
|
||||
"""
|
||||
if "timestamp" in src.columns:
|
||||
return src["timestamp"].to_numpy()
|
||||
d = pd.to_datetime(src["date"])
|
||||
if getattr(d.dt, "tz", None) is None:
|
||||
d = d.dt.tz_localize("UTC")
|
||||
return (d.dt.tz_convert("UTC").dt.tz_localize(None)
|
||||
.astype("datetime64[ms]").astype("int64").to_numpy())
|
||||
|
||||
|
||||
def ensure_timestamp(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""保证 df 带 timestamp 列,缺失时补一份副本,不改动调用方的对象。"""
|
||||
if "timestamp" in df.columns:
|
||||
return df
|
||||
out = df.copy()
|
||||
out["timestamp"] = timestamps_ms(out)
|
||||
return out
|
||||
|
||||
|
||||
def build_htf_zones(df_htf: pd.DataFrame, tf: str, chan=None, avail_bi: int | None = None) -> pd.DataFrame:
|
||||
"""算 pure 笔中枢,返回带生效时间的区间表。
|
||||
|
||||
available_ts —— 该中枢最早可被使用的时间戳(其确认时刻)。
|
||||
传入已构建好的 chan 可避免重复跑一遍 pipeline(大数据集上省一半时间)。
|
||||
"""
|
||||
if chan is None:
|
||||
from chanlun import TF_DF
|
||||
|
||||
chan = TF_DF(df_htf, 1, tf)
|
||||
zs_list = chan.cal_bi_zs_list_pure(chan.bi_list)
|
||||
# 用引擎自己的 dataframe 对齐,避免调用方传入的 df 与引擎内部行数不一致
|
||||
src = chan.dataframe if getattr(chan, "dataframe", None) is not None else df_htf
|
||||
return zones_from_zs_list(zs_list, src, avail_bi=avail_bi)
|
||||
|
||||
|
||||
def zones_from_zs_list(zs_list, src: pd.DataFrame, avail_bi: int | None = None) -> pd.DataFrame:
|
||||
"""把已算好的 pure 笔中枢转成区间表。
|
||||
|
||||
调用方手工跑过 cal_bi_zs_list_pure 时走这里,免得再算一遍(web 的 analyze_chan
|
||||
就是这种用法)。
|
||||
"""
|
||||
ts_of = dict(zip(src["date"].dt.strftime("%Y-%m-%d %H:%M:%S"), timestamps_ms(src)))
|
||||
# 中枢可用时刻取第几笔。在循环外解析一次,别让每个中枢都去读一遍环境变量。
|
||||
i = _resolve_avail_bi(avail_bi)
|
||||
|
||||
rows = []
|
||||
for zs in zs_list:
|
||||
bis = getattr(zs, "bi_list", [])
|
||||
if not bis:
|
||||
continue
|
||||
# 笔数不够时退回最后一笔(i=2 需要至少 3 笔才成立)
|
||||
key_bi = bis[i] if (i == -1 or len(bis) > i) else bis[-1]
|
||||
sure_key = str(getattr(key_bi, "sure_time", "") or "")
|
||||
end_key = str(getattr(key_bi, "end_time", "") or "")
|
||||
avail = ts_of.get(sure_key) or ts_of.get(end_key)
|
||||
if avail is None:
|
||||
continue
|
||||
start_key = str(bis[0].start_time)
|
||||
rows.append({
|
||||
"zg": float(zs.zg), "zd": float(zs.zd),
|
||||
"gg": float(getattr(zs, "gg", zs.zg)), "dd": float(getattr(zs, "dd", zs.zd)),
|
||||
"start_ts": ts_of.get(start_key, avail),
|
||||
"available_ts": int(avail),
|
||||
})
|
||||
out = pd.DataFrame(rows)
|
||||
return out.sort_values("available_ts").reset_index(drop=True) if not out.empty else out
|
||||
|
||||
|
||||
def add_zone_ladder(zones: pd.DataFrame) -> pd.DataFrame:
|
||||
"""标注每个中枢相对前一个中枢是否同向推进(缠论「趋势 vs 盘整」)。
|
||||
|
||||
z_above / z_below 分别对应向上、向下推进。买信号要求 z_above、卖信号要求 z_below
|
||||
时,30m/2h 的 PF 从 2.72 升到 3.41。
|
||||
"""
|
||||
out = zones.copy()
|
||||
if out.empty:
|
||||
out["z_above"], out["z_below"] = pd.Series(dtype=bool), pd.Series(dtype=bool)
|
||||
return out
|
||||
pg, pdn = out["zg"].shift(), out["zd"].shift()
|
||||
out["z_above"] = (out["zd"] > pg).fillna(False)
|
||||
out["z_below"] = (out["zg"] < pdn).fillna(False)
|
||||
return out
|
||||
|
||||
|
||||
def htf_fx_timeline(chan_htf, df_htf: pd.DataFrame | None = None) -> pd.DataFrame:
|
||||
"""把大级别分型压成一条按确认时间排序的时间线。
|
||||
|
||||
confirm_ts 是该分型最早可被使用的时刻。timestamp 是K线开盘时刻,而分型要等这根K线
|
||||
收盘才算数,所以整体后移一个大级别周期;否则小级别会提前一整根大级别K线拿到信号。
|
||||
|
||||
只取方向与确认时刻——同向过滤用不到背驰强度,省掉 MACD 面积计算。
|
||||
"""
|
||||
src = chan_htf.dataframe if getattr(chan_htf, "dataframe", None) is not None else df_htf
|
||||
if src is None or len(src) == 0:
|
||||
return pd.DataFrame(columns=["confirm_ts", "fx_ts", "direction", "price"])
|
||||
ts = timestamps_ms(src)
|
||||
idx_of = {t: i for i, t in enumerate(src["date"].dt.strftime("%Y-%m-%d %H:%M:%S"))}
|
||||
period = int(np.median(np.diff(ts))) if len(ts) > 1 else 0
|
||||
|
||||
rows = []
|
||||
for klc in getattr(chan_htf, "klc_list", []):
|
||||
if klc.fx not in (Chan_FX_TYPE.TOP, Chan_FX_TYPE.BOTTOM):
|
||||
continue
|
||||
if klc.next is None or klc.next.end_klu is None:
|
||||
continue
|
||||
e_key, c_key = str(klc.end_time), str(klc.next.end_klu.time)
|
||||
if e_key not in idx_of or c_key not in idx_of:
|
||||
continue
|
||||
fx_idx, confirm_idx = idx_of[e_key], idx_of[c_key]
|
||||
if confirm_idx <= fx_idx:
|
||||
continue
|
||||
d = 1 if klc.fx == Chan_FX_TYPE.BOTTOM else -1
|
||||
rows.append({
|
||||
"confirm_ts": int(ts[confirm_idx]) + period,
|
||||
"fx_ts": int(ts[fx_idx]),
|
||||
"direction": d,
|
||||
"price": float(klc.low if d == 1 else klc.high),
|
||||
})
|
||||
out = pd.DataFrame(rows)
|
||||
if out.empty:
|
||||
return pd.DataFrame(columns=["confirm_ts", "fx_ts", "direction", "price"])
|
||||
return out.sort_values("confirm_ts").reset_index(drop=True)
|
||||
|
||||
|
||||
def attach_htf_agree(sig: pd.DataFrame, df_ltf: pd.DataFrame, tl: pd.DataFrame) -> pd.DataFrame:
|
||||
"""给每个小级别信号挂上「入场时刻之前最近的大级别分型」是否同向。
|
||||
|
||||
产出 htf_dir(+1 底 / -1 顶)与 htf_agree(1 同向 / 0 反向 / NaN 无可用分型)。
|
||||
"""
|
||||
out = sig.copy()
|
||||
if sig.empty or tl.empty:
|
||||
out["htf_dir"] = np.nan
|
||||
out["htf_agree"] = np.nan
|
||||
return out
|
||||
|
||||
ts_ltf = timestamps_ms(df_ltf)
|
||||
entry_ts = ts_ltf[out["entry_idx"].to_numpy().astype(int)]
|
||||
k = np.searchsorted(tl["confirm_ts"].to_numpy(), entry_ts, side="right") - 1
|
||||
valid = k >= 0
|
||||
k_safe = np.clip(k, 0, len(tl) - 1)
|
||||
|
||||
fx_dir = tl["direction"].to_numpy()[k_safe].astype(float)
|
||||
out["htf_dir"] = np.where(valid, fx_dir, np.nan)
|
||||
out["htf_agree"] = np.where(
|
||||
valid, (fx_dir == out["direction"].to_numpy()).astype(float), np.nan
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def attach_zone_ladder(sig: pd.DataFrame, zones: pd.DataFrame) -> pd.DataFrame:
|
||||
"""按信号方向取该中枢的阶梯标记:买看 z_above、卖看 z_below。
|
||||
|
||||
zone_i 是 find_fast_bsp3 内 enumerate 出的位置序号,故用 iloc 定位。
|
||||
"""
|
||||
out = sig.copy()
|
||||
if sig.empty:
|
||||
out["ladder_ok"] = pd.Series(dtype=bool)
|
||||
return out
|
||||
z = zones if "z_above" in zones.columns else add_zone_ladder(zones)
|
||||
above = z["z_above"].to_numpy()
|
||||
below = z["z_below"].to_numpy()
|
||||
zi = out["zone_i"].to_numpy().astype(int)
|
||||
ok = np.where(out["direction"].to_numpy() == 1, above[zi], below[zi])
|
||||
out["ladder_ok"] = ok.astype(bool)
|
||||
return out
|
||||
|
||||
|
||||
def find_fast_bsp3(
|
||||
df: pd.DataFrame,
|
||||
zones: pd.DataFrame,
|
||||
scan: int = 200,
|
||||
pullback_win: int = 30,
|
||||
tol: float = -1.0,
|
||||
max_per_zone: int = 1,
|
||||
diag: dict | None = None,
|
||||
require_touch: bool = False,
|
||||
) -> pd.DataFrame:
|
||||
"""扫描每个中枢,找突破后回抽不回中枢的入场点。
|
||||
|
||||
zones 需含 zg / zd / available_ts,且 available_ts 已是可用时刻。
|
||||
max_per_zone > 1 时,同一中枢在首次入场后继续往后找二次、三次突破回抽,
|
||||
用来检验「趋势里同一中枢反复给机会」是否值得做。
|
||||
|
||||
tol 是「算作回抽中」的边界容差。require_touch=False 时它不再是入场门槛,
|
||||
仅决定哪些K线被视为回抽中而跳过转强判定,故 tol 越大入场越晚。
|
||||
默认负值 = 完全禁用该跳过,突破后每根都检查转强,滞后压到 2.2 根。
|
||||
step34 实测滞后与收益严格单调:9.9根 PF1.88 / 5.8根 2.37 / 2.2根 2.86。
|
||||
|
||||
返回列:
|
||||
entry_idx 实时可下单的K线
|
||||
direction +1 三买 / -1 三卖
|
||||
bo_idx 突破根
|
||||
pb_idx 回抽极值根
|
||||
lag entry_idx - bo_idx
|
||||
depth 回抽深度(相对中枢边界,负值表示曾插入中枢)
|
||||
occ 这是该中枢的第几次入场
|
||||
"""
|
||||
if zones.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
ts = df["timestamp"].to_numpy()
|
||||
close = df["close"].to_numpy(dtype=float)
|
||||
high = df["high"].to_numpy(dtype=float)
|
||||
low = df["low"].to_numpy(dtype=float)
|
||||
n = len(df)
|
||||
rows = []
|
||||
|
||||
def note(key: str) -> None:
|
||||
if diag is not None:
|
||||
diag[key] = diag.get(key, 0) + 1
|
||||
|
||||
for zone_i, (_, z) in enumerate(zones.iterrows()):
|
||||
note("中枢总数")
|
||||
zg, zd = float(z["zg"]), float(z["zd"])
|
||||
if zg <= zd:
|
||||
note("×无效中枢")
|
||||
continue
|
||||
start = int(np.searchsorted(ts, z["available_ts"], side="left"))
|
||||
if start >= n - 2:
|
||||
note("×中枢太靠后")
|
||||
continue
|
||||
# 允许多次入场时按比例放宽扫描窗口,否则后几次机会会被窗口截断
|
||||
scan_end = min(start + scan * max_per_zone, n)
|
||||
cursor = start
|
||||
|
||||
for occ in range(1, max_per_zone + 1):
|
||||
if cursor >= n - 2:
|
||||
break
|
||||
# 第一步:找突破。要求突破前确实待在中枢内,避免把远处的价格当突破。
|
||||
was_inside = False
|
||||
bo_idx, d = None, 0
|
||||
for j in range(cursor, scan_end):
|
||||
c = close[j]
|
||||
if zd <= c <= zg:
|
||||
was_inside = True
|
||||
continue
|
||||
if not was_inside:
|
||||
continue
|
||||
bo_idx, d = j, (1 if c > zg else -1)
|
||||
break
|
||||
if bo_idx is None:
|
||||
if occ == 1:
|
||||
note("×窗口内未突破")
|
||||
break
|
||||
|
||||
edge = zg if d == 1 else zd
|
||||
# 第二步:突破后监控回抽,回抽不跌回中枢且重新顺势 -> 入场
|
||||
touched = False
|
||||
pb_idx = None
|
||||
pb_ext = None
|
||||
entry_idx = None
|
||||
fell_back = False
|
||||
for j in range(bo_idx + 1, min(bo_idx + pullback_win + 1, n)):
|
||||
# 收盘跌回中枢 -> 突破失效
|
||||
if zd <= close[j] <= zg:
|
||||
fell_back = True
|
||||
break
|
||||
# 回抽触及边界附近(允许 tol 的毛刺)
|
||||
near = (low[j] <= edge * (1 + tol)) if d == 1 else (high[j] >= edge * (1 - tol))
|
||||
if near:
|
||||
touched = True
|
||||
ext = low[j] if d == 1 else high[j]
|
||||
if pb_ext is None or ((ext < pb_ext) if d == 1 else (ext > pb_ext)):
|
||||
pb_ext, pb_idx = ext, j
|
||||
continue
|
||||
# 回抽后重新顺势:收盘创出前一根之上(三买)/ 之下(三卖)
|
||||
# require_touch=False 时不强求回抽碰到中枢边界,
|
||||
# 这样「突破后一去不回头」的强势段也能收进来。
|
||||
if (touched and pb_idx is not None) or not require_touch:
|
||||
go = close[j] > high[j - 1] if d == 1 else close[j] < low[j - 1]
|
||||
if go:
|
||||
entry_idx = j
|
||||
break
|
||||
if entry_idx is None:
|
||||
if occ == 1:
|
||||
note("×突破后跌回中枢" if fell_back
|
||||
else "×回抽未触及边界" if not touched
|
||||
else "×触及边界但未转强")
|
||||
# 这次突破没走成,从突破点之后继续找下一次
|
||||
cursor = bo_idx + 1
|
||||
continue
|
||||
if pb_ext is None:
|
||||
# 未触及边界就转强(require_touch=False):取突破至入场间的实际极值
|
||||
seg = slice(bo_idx + 1, entry_idx + 1)
|
||||
pb_ext = float(low[seg].min() if d == 1 else high[seg].max())
|
||||
pb_idx = int((low[seg].argmin() if d == 1 else high[seg].argmax())
|
||||
+ bo_idx + 1)
|
||||
if occ == 1:
|
||||
note("√成交")
|
||||
|
||||
# 回抽深度:>0 表示未插入中枢,越大表示回抽越浅
|
||||
depth = (pb_ext - zg) / zg if d == 1 else (zd - pb_ext) / zd
|
||||
rows.append({
|
||||
"entry_idx": entry_idx, "direction": d,
|
||||
"bo_idx": bo_idx, "pb_idx": pb_idx,
|
||||
"lag": entry_idx - bo_idx,
|
||||
"depth": depth,
|
||||
"zg": zg, "zd": zd,
|
||||
"width_pct": (zg - zd) / close[bo_idx],
|
||||
"occ": occ,
|
||||
"zone_i": zone_i,
|
||||
})
|
||||
cursor = entry_idx + 1
|
||||
|
||||
out = pd.DataFrame(rows)
|
||||
if out.empty:
|
||||
return out
|
||||
# 相邻中枢可能突破到同一根K线,同一时刻只能有一个仓位,保留最早成型的那个
|
||||
return (out.sort_values(["entry_idx", "occ", "zone_i"])
|
||||
.drop_duplicates("entry_idx", keep="first")
|
||||
.reset_index(drop=True))
|
||||
@@ -0,0 +1,7 @@
|
||||
"""威科夫分析(启发式):交易区间 / 阶段 / 事件 / Volume Profile / Live。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .engine import analyze_wyckoff
|
||||
from .live import execution_signal_from_wyckoff
|
||||
|
||||
__all__ = ["analyze_wyckoff", "execution_signal_from_wyckoff"]
|
||||
@@ -0,0 +1,196 @@
|
||||
"""威科夫分析入口:Cycle → Phase → Event → VP + Live(MULTI-CYCLE / LIVE-STRUCTURE)。
|
||||
|
||||
range.py 只产 TradingRange;Confirmed 走 events.py;Live 走 live.py。
|
||||
cycles[0]=ACTIVE;禁止 cycles[-1] 取 active。
|
||||
Execution 只消费 Confirmed(见 live.execution_signal_from_wyckoff)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from .events import build_phases, detect_bias_and_events
|
||||
from .live import analyze_live_structure
|
||||
from .range import detect_trading_ranges
|
||||
from .volume_profile import compute_volume_profile
|
||||
|
||||
|
||||
def _fmt_time(v) -> Optional[str]:
|
||||
if v is None:
|
||||
return None
|
||||
if hasattr(v, "isoformat"):
|
||||
try:
|
||||
return v.isoformat()
|
||||
except Exception:
|
||||
pass
|
||||
return str(v)
|
||||
|
||||
|
||||
def _empty(vp_bins: int) -> Dict[str, Any]:
|
||||
return {
|
||||
"cycles": [],
|
||||
"trading_range": None,
|
||||
"bias": "unknown",
|
||||
"phases": [],
|
||||
"events": [],
|
||||
"volume_profile": {"bins": [], "poc": None, "vah": None, "val": None, "bin_count": vp_bins},
|
||||
"volume_confirm": {"avg_volume": 0.0, "event_checks": {}},
|
||||
"live": None,
|
||||
}
|
||||
|
||||
|
||||
def _confidence_for_confirmed(
|
||||
tr: Dict[str, Any],
|
||||
phases: List[Dict[str, Any]],
|
||||
events: List[Dict[str, Any]],
|
||||
) -> Dict[str, float]:
|
||||
range_c = float(tr.get("range_confidence") or 0.5)
|
||||
labels = {p.get("phase") for p in phases}
|
||||
phase_c = 0.35
|
||||
if "A" in labels and "B" in labels:
|
||||
phase_c += 0.15
|
||||
if "C" in labels:
|
||||
phase_c += 0.2
|
||||
if "D" in labels or "E" in labels:
|
||||
phase_c += 0.15
|
||||
phase_c = min(0.95, phase_c)
|
||||
types = {e.get("type") for e in events}
|
||||
event_c = 0.25
|
||||
for t in ("Spring", "UTAD", "SOS", "SOW", "LPS", "LPSY"):
|
||||
if t in types:
|
||||
event_c += 0.12
|
||||
event_c = min(0.95, event_c)
|
||||
overall = 0.4 * range_c + 0.3 * phase_c + 0.3 * event_c
|
||||
return {
|
||||
"range": round(range_c, 3),
|
||||
"phase": round(phase_c, 3),
|
||||
"event": round(event_c, 3),
|
||||
"overall": round(overall, 3),
|
||||
}
|
||||
|
||||
|
||||
def _build_cycle(
|
||||
work: pd.DataFrame,
|
||||
tr: Dict[str, Any],
|
||||
cycle_id: int,
|
||||
vp_bins: int,
|
||||
) -> Dict[str, Any]:
|
||||
bias, events, volume_confirm = detect_bias_and_events(work, tr)
|
||||
phases = build_phases(work, tr, bias, events)
|
||||
vp = compute_volume_profile(
|
||||
work,
|
||||
int(tr["abs_start_idx"]),
|
||||
int(tr["abs_end_idx"]),
|
||||
bin_count=vp_bins,
|
||||
)
|
||||
for ev in events:
|
||||
ev["time"] = _fmt_time(ev.get("time"))
|
||||
for ph in phases:
|
||||
ph["start_time"] = _fmt_time(ph.get("start_time"))
|
||||
ph["end_time"] = _fmt_time(ph.get("end_time"))
|
||||
|
||||
is_active = cycle_id == 0
|
||||
trading_range = {
|
||||
"start_time": _fmt_time(tr.get("start_time")),
|
||||
"end_time": _fmt_time(tr.get("end_time")),
|
||||
"high": float(tr["high"]),
|
||||
"low": float(tr["low"]),
|
||||
"mid": float(tr["mid"]),
|
||||
"active": bool(is_active),
|
||||
"bars": int(tr.get("bars", 0)),
|
||||
}
|
||||
conf = _confidence_for_confirmed(tr, phases, events)
|
||||
|
||||
# Live 层:仅 ACTIVE 周期做推演;历史周期归档为 COMPLETED
|
||||
if is_active:
|
||||
live = analyze_live_structure(
|
||||
work, tr, confirmed_events=events, confirmed_phases=phases, bias=bias,
|
||||
)
|
||||
lifecycle = live.get("lifecycle") or "FORMING"
|
||||
else:
|
||||
live = None
|
||||
lifecycle = "COMPLETED"
|
||||
|
||||
return {
|
||||
"id": int(cycle_id),
|
||||
"role": "latest" if is_active else "historical",
|
||||
# MULTI-CYCLE:时间线角色
|
||||
"status": "ACTIVE" if is_active else "HISTORICAL",
|
||||
# LIVE-STRUCTURE:生命周期
|
||||
"lifecycle": lifecycle,
|
||||
"direction": "latest" if is_active else "historical",
|
||||
"period": {
|
||||
"start_time": _fmt_time(tr.get("start_time")),
|
||||
"end_time": _fmt_time(tr.get("end_time")),
|
||||
"bars": int(tr.get("bars", 0)),
|
||||
},
|
||||
"confidence": conf,
|
||||
"trading_range": trading_range,
|
||||
"bias": bias,
|
||||
# 兼容旧读法:顶层 phases/events = confirmed
|
||||
"phases": phases,
|
||||
"events": events,
|
||||
"confirmed": {
|
||||
"phases": phases,
|
||||
"events": events,
|
||||
"volume_confirm": volume_confirm,
|
||||
},
|
||||
"live": live,
|
||||
"volume_profile": vp,
|
||||
"volume_confirm": volume_confirm,
|
||||
}
|
||||
|
||||
|
||||
def analyze_wyckoff(
|
||||
df: pd.DataFrame,
|
||||
lookback: int = 120,
|
||||
vp_bins: int = 50,
|
||||
min_bars: int = 24,
|
||||
atr_mult: float = 1.2,
|
||||
range_start_time=None,
|
||||
prefer_start_time=None,
|
||||
max_cycles: int = 8,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
多周期威科夫分析。
|
||||
cycles[0] = ACTIVE;顶层 phases/events 只镜像 Confirmed。
|
||||
顶层 live 镜像 cycles[0].live。
|
||||
"""
|
||||
empty = _empty(vp_bins)
|
||||
if df is None or len(df) < 30:
|
||||
return empty
|
||||
if not all(c in df.columns for c in ("open", "high", "low", "close")):
|
||||
return empty
|
||||
work = df.copy()
|
||||
if "volume" not in work.columns:
|
||||
work["volume"] = 1.0
|
||||
|
||||
trs = detect_trading_ranges(
|
||||
work,
|
||||
lookback=lookback,
|
||||
min_bars=max(8, int(min_bars)),
|
||||
atr_mult=atr_mult,
|
||||
max_cycles=max(1, min(8, int(max_cycles))),
|
||||
prefer_start_time=prefer_start_time,
|
||||
range_start_time=range_start_time,
|
||||
)
|
||||
if not trs:
|
||||
return empty
|
||||
|
||||
cycles: List[Dict[str, Any]] = []
|
||||
for i, tr in enumerate(trs):
|
||||
cycles.append(_build_cycle(work, tr, cycle_id=i, vp_bins=vp_bins))
|
||||
|
||||
active = cycles[0]
|
||||
return {
|
||||
"cycles": cycles,
|
||||
"trading_range": active["trading_range"],
|
||||
"bias": active["bias"],
|
||||
"phases": active["confirmed"]["phases"],
|
||||
"events": active["confirmed"]["events"],
|
||||
"volume_profile": active["volume_profile"],
|
||||
"volume_confirm": active["volume_confirm"],
|
||||
"live": active.get("live"),
|
||||
"lifecycle": active.get("lifecycle"),
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
"""威科夫阶段与事件(启发式)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def _bar_time(df: pd.DataFrame, i: int):
|
||||
row = df.iloc[i]
|
||||
if "date" in df.columns and pd.notna(row["date"]):
|
||||
return row["date"]
|
||||
if "timestamp" in df.columns:
|
||||
return row["timestamp"]
|
||||
return i
|
||||
|
||||
|
||||
def _avg_vol(df: pd.DataFrame, i: int, win: int = 20) -> float:
|
||||
a = max(0, i - win + 1)
|
||||
v = df["volume"].astype(float).iloc[a : i + 1]
|
||||
m = float(v.mean()) if len(v) else 0.0
|
||||
return m if m > 0 else 1.0
|
||||
|
||||
|
||||
def detect_bias_and_events(
|
||||
df: pd.DataFrame,
|
||||
tr: Dict[str, Any],
|
||||
) -> Tuple[str, List[Dict[str, Any]], Dict[str, Any]]:
|
||||
"""
|
||||
返回 bias、events、volume_confirm。
|
||||
|
||||
Spring/UTAD 相对「结构高低」判定:取区间内次低/次高(剔除单根极值),
|
||||
避免箱体把假破低点吃进 lo 后永远刺不破、从而无 C 阶段。
|
||||
"""
|
||||
hi = float(tr["high"])
|
||||
lo = float(tr["low"])
|
||||
mid = float(tr["mid"])
|
||||
tol = float(tr.get("tol") or (hi - lo) * 0.05)
|
||||
s = int(tr["abs_start_idx"])
|
||||
e = int(tr["abs_end_idx"])
|
||||
events: List[Dict[str, Any]] = []
|
||||
|
||||
# 结构边界:用次低/次高作假破参照(至少 8 根才启用)
|
||||
seg = df.iloc[s : e + 1]
|
||||
event_lo, event_hi = lo, hi
|
||||
if len(seg) >= 8:
|
||||
lows = seg["low"].astype(float)
|
||||
highs = seg["high"].astype(float)
|
||||
# nsmallest(2) 的较大者 = 次低;nlargest(2) 的较小者 = 次高
|
||||
event_lo = float(lows.nsmallest(min(2, len(lows))).iloc[-1])
|
||||
event_hi = float(highs.nlargest(min(2, len(highs))).iloc[-1])
|
||||
# 勿比公布箱沿更「松」:结构带应在箱内
|
||||
event_lo = max(event_lo, lo)
|
||||
event_hi = min(event_hi, hi)
|
||||
# 若次低仍等于极值(多根同价),略抬参照便于识别收回
|
||||
if abs(event_lo - lo) < 1e-12:
|
||||
event_lo = lo + max(tol * 0.35, (hi - lo) * 0.02)
|
||||
if abs(event_hi - hi) < 1e-12:
|
||||
event_hi = hi - max(tol * 0.35, (hi - lo) * 0.02)
|
||||
|
||||
# 扫描区间内及之后(含 tail_reserve)
|
||||
scan_end = int(tr.get("abs_scan_end_idx", min(len(df) - 1, e + 15)))
|
||||
scan_end = min(len(df) - 1, max(scan_end, e))
|
||||
spring = None
|
||||
utad = None
|
||||
sos = None
|
||||
sod = None # sign of weakness / distribution breakdown
|
||||
lps = None
|
||||
lpsy = None
|
||||
|
||||
for i in range(s + 2, scan_end + 1):
|
||||
row = df.iloc[i]
|
||||
low = float(row["low"])
|
||||
high = float(row["high"])
|
||||
close = float(row["close"])
|
||||
vol = float(row["volume"]) if "volume" in df.columns else 0.0
|
||||
avg_v = _avg_vol(df, i)
|
||||
ratio = vol / avg_v if avg_v else 0.0
|
||||
|
||||
# Spring: pierce below structural support then close back
|
||||
if spring is None and low < event_lo - tol * 0.35 and close >= event_lo - tol * 0.35:
|
||||
vol_ok = ratio <= 1.35 or (i + 1 <= scan_end and float(df.iloc[min(i + 1, scan_end)]["volume"]) / avg_v < 1.2)
|
||||
spring = {
|
||||
"type": "Spring",
|
||||
"time": _bar_time(df, i),
|
||||
"price": low,
|
||||
"note": "假破下沿后收回",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
|
||||
# UTAD: pierce above structural resistance then close back
|
||||
if utad is None and high > event_hi + tol * 0.35 and close <= event_hi + tol * 0.35:
|
||||
vol_ok = ratio >= 0.8
|
||||
utad = {
|
||||
"type": "UTAD",
|
||||
"time": _bar_time(df, i),
|
||||
"price": high,
|
||||
"note": "假破上沿后跌回",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
|
||||
# SOS: close above high with volume
|
||||
if sos is None and close > hi + tol * 0.15:
|
||||
vol_ok = ratio >= 1.15
|
||||
sos = {
|
||||
"type": "SOS",
|
||||
"time": _bar_time(df, i),
|
||||
"price": close,
|
||||
"note": "放量上破交易区间",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
|
||||
# SOW / breakdown
|
||||
if sod is None and close < lo - tol * 0.15:
|
||||
vol_ok = ratio >= 1.15
|
||||
sod = {
|
||||
"type": "SOW",
|
||||
"time": _bar_time(df, i),
|
||||
"price": close,
|
||||
"note": "放量下破交易区间",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
|
||||
# LPS after SOS: pullback that holds above mid/high-band with lighter volume
|
||||
if sos is not None:
|
||||
si = int(sos["idx"])
|
||||
for i in range(si + 1, min(len(df), si + 25)):
|
||||
row = df.iloc[i]
|
||||
low = float(row["low"])
|
||||
close = float(row["close"])
|
||||
vol = float(row["volume"]) if "volume" in df.columns else 0.0
|
||||
avg_v = _avg_vol(df, i)
|
||||
ratio = vol / avg_v if avg_v else 0.0
|
||||
if low >= mid - tol and close >= hi - tol * 2:
|
||||
vol_ok = ratio <= 1.05
|
||||
lps = {
|
||||
"type": "LPS",
|
||||
"time": _bar_time(df, i),
|
||||
"price": low,
|
||||
"note": "突破后缩量回踩不破",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
break
|
||||
|
||||
if sod is not None:
|
||||
si = int(sod["idx"])
|
||||
for i in range(si + 1, min(len(df), si + 25)):
|
||||
row = df.iloc[i]
|
||||
high = float(row["high"])
|
||||
close = float(row["close"])
|
||||
vol = float(row["volume"]) if "volume" in df.columns else 0.0
|
||||
avg_v = _avg_vol(df, i)
|
||||
ratio = vol / avg_v if avg_v else 0.0
|
||||
if high <= mid + tol and close <= lo + tol * 2:
|
||||
vol_ok = ratio <= 1.05
|
||||
lpsy = {
|
||||
"type": "LPSY",
|
||||
"time": _bar_time(df, i),
|
||||
"price": high,
|
||||
"note": "下跌突破后缩量反抽不过",
|
||||
"volume_ratio": round(ratio, 3),
|
||||
"volume_ok": bool(vol_ok),
|
||||
"idx": i,
|
||||
}
|
||||
break
|
||||
|
||||
# 冲突清理:已判定吸筹且有 SOS 时,丢弃更早的 UTAD(避免阶段/图面误导)
|
||||
# 派发且有 SOW 时,丢弃更晚才合理的 Spring 假信号同理在偏置后再滤
|
||||
keep = []
|
||||
for ev in (spring, sos, lps, utad, sod, lpsy):
|
||||
if not ev:
|
||||
continue
|
||||
keep.append(ev)
|
||||
|
||||
# bias(先算)
|
||||
last_c = float(df["close"].iloc[-1])
|
||||
bias = "unknown"
|
||||
if sos and (not sod or int(sos.get("idx", 0)) >= int(sod.get("idx", 0))):
|
||||
bias = "accumulation"
|
||||
elif sod and (not sos or int(sod.get("idx", 0)) > int(sos.get("idx", 0))):
|
||||
bias = "distribution"
|
||||
elif spring and not utad:
|
||||
bias = "accumulation"
|
||||
elif utad and not spring:
|
||||
bias = "distribution"
|
||||
elif last_c >= mid:
|
||||
bias = "accumulation"
|
||||
else:
|
||||
bias = "distribution"
|
||||
|
||||
filtered = []
|
||||
for ev in keep:
|
||||
if bias == "accumulation" and ev["type"] == "UTAD" and sos and int(ev["idx"]) <= int(sos["idx"]):
|
||||
continue
|
||||
if bias == "distribution" and ev["type"] == "Spring" and sod and int(ev["idx"]) <= int(sod["idx"]):
|
||||
continue
|
||||
filtered.append(ev)
|
||||
|
||||
events = [{k: v for k, v in ev.items() if k != "idx"} for ev in filtered]
|
||||
|
||||
avg_volume = float(df["volume"].astype(float).iloc[max(0, e - 20) : e + 1].mean()) if "volume" in df.columns else 0.0
|
||||
volume_confirm = {
|
||||
"avg_volume": avg_volume,
|
||||
"event_checks": {ev["type"]: {"volume_ok": ev.get("volume_ok"), "volume_ratio": ev.get("volume_ratio")} for ev in events},
|
||||
}
|
||||
return bias, events, volume_confirm
|
||||
|
||||
|
||||
def build_phases(
|
||||
df: pd.DataFrame,
|
||||
tr: Dict[str, Any],
|
||||
bias: str,
|
||||
events: List[Dict[str, Any]],
|
||||
min_bars: int = 3,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
按威科夫事件锚点切分 A–E(启发式)。
|
||||
|
||||
吸筹:A停止 → B筑底 → C测试(Spring) → D拉升(SOS…LPS) → E离开
|
||||
派发:A停止 → B筑顶 → C测试(UTAD) → D派发(SOW…LPSY) → E离开
|
||||
|
||||
无 Spring/UTAD 时:若已有 SOS/SOW,用突破前末次沿带测试补 C;仍无则省略 C。
|
||||
"""
|
||||
s = int(tr["abs_start_idx"])
|
||||
e = int(tr["abs_end_idx"])
|
||||
hi = float(tr["high"])
|
||||
lo = float(tr["low"])
|
||||
n_last = len(df) - 1
|
||||
min_span = max(2, min_bars - 1)
|
||||
range_len = max(1, e - s)
|
||||
|
||||
def _match_idx(t) -> Optional[int]:
|
||||
if t is None:
|
||||
return None
|
||||
lo = max(0, s - 2)
|
||||
hi = min(len(df), e + 40)
|
||||
for i in range(lo, hi):
|
||||
if _bar_time(df, i) == t:
|
||||
return i
|
||||
try:
|
||||
tt = pd.Timestamp(t)
|
||||
sample = None
|
||||
if "date" in df.columns and len(df):
|
||||
sample = df["date"].iloc[min(s, n_last)]
|
||||
if sample is not None and getattr(sample, "tzinfo", None) is not None and tt.tzinfo is None:
|
||||
tt = tt.tz_localize(sample.tzinfo)
|
||||
for i in range(lo, hi):
|
||||
bt = _bar_time(df, i)
|
||||
try:
|
||||
if abs((pd.Timestamp(bt) - tt).total_seconds()) <= 1:
|
||||
return i
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
event_idx: Dict[str, int] = {}
|
||||
for ev in events:
|
||||
idx = _match_idx(ev.get("time"))
|
||||
if idx is not None:
|
||||
event_idx[str(ev.get("type"))] = idx
|
||||
|
||||
accum = bias != "distribution"
|
||||
if accum:
|
||||
c_ev = event_idx.get("Spring")
|
||||
d_ev = event_idx.get("SOS")
|
||||
d_tail = event_idx.get("LPS") or d_ev
|
||||
else:
|
||||
c_ev = event_idx.get("UTAD")
|
||||
d_ev = event_idx.get("SOW")
|
||||
d_tail = event_idx.get("LPSY") or d_ev
|
||||
|
||||
# 有 D 无明确测试事件时:用突破前最后一次触及下/上沿作为 C(次级测试)
|
||||
if c_ev is None and d_ev is not None:
|
||||
band = lo + (hi - lo) * 0.28 if accum else hi - (hi - lo) * 0.28
|
||||
for i in range(int(d_ev) - 1, s + 1, -1):
|
||||
row = df.iloc[i]
|
||||
if accum and float(row["low"]) <= band:
|
||||
c_ev = i
|
||||
break
|
||||
if not accum and float(row["high"]) >= band:
|
||||
c_ev = i
|
||||
break
|
||||
|
||||
def _lab(phase: str) -> str:
|
||||
if accum:
|
||||
m = {"A": "A停止下跌", "B": "B筑底", "C": "C测试", "D": "D拉升", "E": "E离开"}
|
||||
else:
|
||||
m = {"A": "A停止上涨", "B": "B筑顶", "C": "C测试", "D": "D派发", "E": "E离开"}
|
||||
return m.get(phase, phase)
|
||||
|
||||
a_end = s + max(min_bars, range_len // 5)
|
||||
|
||||
c_start = c_end = None
|
||||
if c_ev is not None:
|
||||
c_start = max(s, int(c_ev) - 1)
|
||||
c_end = min(n_last, int(c_ev) + 1)
|
||||
|
||||
if d_ev is not None:
|
||||
d_start = int(d_ev)
|
||||
d_end = min(n_last, max(int(d_tail or d_ev), d_start) + max(min_bars, range_len // 8))
|
||||
if d_tail is not None:
|
||||
d_end = max(d_end, min(n_last, int(d_tail) + 1))
|
||||
else:
|
||||
d_start = d_end = None
|
||||
|
||||
if c_start is not None:
|
||||
b_end = max(a_end + 1, c_start)
|
||||
elif d_start is not None:
|
||||
b_end = max(a_end + 1, d_start)
|
||||
else:
|
||||
b_end = max(a_end + 1, e)
|
||||
|
||||
if d_end is not None:
|
||||
e_start = min(n_last, d_end)
|
||||
e_end = n_last
|
||||
else:
|
||||
e_start = e_end = None
|
||||
|
||||
raw = [("A", s, a_end), ("B", a_end, b_end)]
|
||||
if c_start is not None and c_end is not None:
|
||||
raw.append(("C", c_start, c_end))
|
||||
if d_start is not None and d_end is not None:
|
||||
raw.append(("D", d_start, d_end))
|
||||
if e_start is not None and e_end is not None and e_end > e_start:
|
||||
raw.append(("E", e_start, e_end))
|
||||
|
||||
phases: List[Dict[str, Any]] = []
|
||||
cursor = s
|
||||
for phase, _a, _b in raw:
|
||||
if cursor >= n_last:
|
||||
break
|
||||
a = max(int(_a), cursor)
|
||||
b = int(max(int(_b), a))
|
||||
need = 1 if phase == "C" else min_span
|
||||
if b < a + need:
|
||||
b = min(n_last, a + need)
|
||||
b = int(np.clip(b, a, n_last))
|
||||
if b < a:
|
||||
continue
|
||||
if phases and phases[-1].get("_a") == a and phases[-1].get("_b") == b:
|
||||
continue
|
||||
phases.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"label": _lab(phase),
|
||||
"start_time": _bar_time(df, a),
|
||||
"end_time": _bar_time(df, b),
|
||||
"_a": a,
|
||||
"_b": b,
|
||||
}
|
||||
)
|
||||
cursor = b
|
||||
for p in phases:
|
||||
p.pop("_a", None)
|
||||
p.pop("_b", None)
|
||||
return phases
|
||||
@@ -0,0 +1,258 @@
|
||||
"""威科夫 Live / Developing 层(WYCKOFF-LIVE-STRUCTURE-001)。
|
||||
|
||||
独立于 Confirmed Engine:不修改 events 确认条件,不写入 confirmed.events。
|
||||
Execution 不得消费本模块输出。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def _avg_vol(df: pd.DataFrame, i: int, win: int = 20) -> float:
|
||||
a = max(0, i - win + 1)
|
||||
v = df["volume"].astype(float).iloc[a : i + 1]
|
||||
m = float(v.mean()) if len(v) else 0.0
|
||||
return m if m > 0 else 1.0
|
||||
|
||||
|
||||
def _empty_live() -> Dict[str, Any]:
|
||||
return {
|
||||
"lifecycle": "UNKNOWN",
|
||||
"range_formation": None,
|
||||
"phase_candidate": None,
|
||||
"event_candidates": [],
|
||||
"next_expected": None,
|
||||
"confidence": {
|
||||
"cycle": 0.0,
|
||||
"phase": 0.0,
|
||||
"event": 0.0,
|
||||
"structure": 0.0,
|
||||
"volume": 0.0,
|
||||
"overall": 0.0,
|
||||
},
|
||||
"note": "",
|
||||
}
|
||||
|
||||
|
||||
def analyze_live_structure(
|
||||
df: pd.DataFrame,
|
||||
tr: Optional[Dict[str, Any]],
|
||||
confirmed_events: Optional[List[Dict[str, Any]]] = None,
|
||||
confirmed_phases: Optional[List[Dict[str, Any]]] = None,
|
||||
bias: str = "unknown",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
基于当前 TradingRange 与已确认事件,推演 Live candidates。
|
||||
confirmed_* 只读,用于避免重复提示已确认事件,不修改之。
|
||||
"""
|
||||
out = _empty_live()
|
||||
if df is None or len(df) < 20 or tr is None:
|
||||
out["note"] = "insufficient structure"
|
||||
return out
|
||||
|
||||
confirmed_events = confirmed_events or []
|
||||
confirmed_phases = confirmed_phases or []
|
||||
confirmed_types: Set[str] = {str(e.get("type")) for e in confirmed_events if e.get("type")}
|
||||
|
||||
s = int(tr["abs_start_idx"])
|
||||
e = int(tr["abs_end_idx"])
|
||||
scan_end = int(tr.get("abs_scan_end_idx", len(df) - 1))
|
||||
scan_end = min(len(df) - 1, max(scan_end, e))
|
||||
hi = float(tr["high"])
|
||||
lo = float(tr["low"])
|
||||
mid = float(tr["mid"])
|
||||
tol = float(tr.get("tol") or (hi - lo) * 0.05)
|
||||
atr = float(tr.get("atr") or max((hi - lo) * 0.2, 1e-9))
|
||||
|
||||
seg = df.iloc[s : e + 1]
|
||||
if len(seg) < 8:
|
||||
out["note"] = "range too short"
|
||||
return out
|
||||
|
||||
# —— Range Formation(横盘 / 波动收敛)——
|
||||
closes = seg["close"].astype(float)
|
||||
highs = seg["high"].astype(float)
|
||||
lows = seg["low"].astype(float)
|
||||
vols = seg["volume"].astype(float) if "volume" in seg.columns else pd.Series([1.0] * len(seg))
|
||||
half = max(4, len(seg) // 2)
|
||||
vol_early = float(np.std(closes.iloc[:half])) if half > 1 else 0.0
|
||||
vol_late = float(np.std(closes.iloc[-half:])) if half > 1 else 0.0
|
||||
width = hi - lo
|
||||
width_atr = width / atr if atr > 0 else 99.0
|
||||
converging = vol_early > 1e-12 and vol_late < vol_early * 0.85
|
||||
range_ok = 1.2 <= width_atr <= 10.0 and len(seg) >= 16
|
||||
structure_score = 0.35
|
||||
if range_ok:
|
||||
structure_score += 0.25
|
||||
if converging:
|
||||
structure_score += 0.2
|
||||
if width_atr <= 6.0:
|
||||
structure_score += 0.1
|
||||
structure_score = float(min(0.95, structure_score))
|
||||
|
||||
out["range_formation"] = {
|
||||
"potential_trading_range": bool(range_ok),
|
||||
"converging": bool(converging),
|
||||
"width_atr": round(width_atr, 3),
|
||||
"bars": int(len(seg)),
|
||||
}
|
||||
|
||||
# —— 最近 K 形态(Phase C / Event candidates)——
|
||||
i = scan_end
|
||||
row = df.iloc[i]
|
||||
o = float(row["open"])
|
||||
h = float(row["high"])
|
||||
l = float(row["low"])
|
||||
c = float(row["close"])
|
||||
rng = max(h - l, 1e-9)
|
||||
lower_wick = min(o, c) - l
|
||||
upper_wick = h - max(o, c)
|
||||
avg_v = _avg_vol(df, i)
|
||||
vol = float(row["volume"]) if "volume" in df.columns else avg_v
|
||||
vol_ratio = vol / avg_v if avg_v else 1.0
|
||||
volume_score = float(np.clip(1.1 - abs(vol_ratio - 1.0) * 0.35, 0.2, 0.95))
|
||||
|
||||
phase_candidate = None
|
||||
phase_conf = 0.0
|
||||
# Phase C:测低 + 下影 + 缩量(吸筹语境)
|
||||
near_lo = l <= lo + tol * 1.2
|
||||
test_low = l < mid and lower_wick >= rng * 0.35
|
||||
vol_contract = vol_ratio <= 1.05
|
||||
if bias != "distribution" and near_lo and test_low and vol_contract:
|
||||
phase_candidate = "C"
|
||||
phase_conf = 0.55 + (0.1 if lower_wick >= rng * 0.5 else 0) + (0.08 if vol_ratio < 0.9 else 0)
|
||||
# Phase D 候选:价格在箱上半、有上破意图但未确认 SOS
|
||||
elif c >= mid and (h >= hi - tol or c > hi - tol * 0.5):
|
||||
phase_candidate = "D"
|
||||
phase_conf = 0.5 + (0.1 if c > mid else 0)
|
||||
elif c < mid and (l <= lo + tol):
|
||||
phase_candidate = "B"
|
||||
phase_conf = 0.45
|
||||
|
||||
# 已有 confirmed phase 时,candidate 取「下一阶段」提示,不覆盖事实
|
||||
confirmed_phase_set = {str(p.get("phase")) for p in confirmed_phases}
|
||||
if "E" in confirmed_phase_set:
|
||||
phase_candidate = phase_candidate or "E"
|
||||
phase_conf = max(phase_conf, 0.7)
|
||||
elif "D" in confirmed_phase_set and phase_candidate is None:
|
||||
phase_candidate = "D"
|
||||
phase_conf = max(phase_conf, 0.65)
|
||||
|
||||
out["phase_candidate"] = phase_candidate
|
||||
phase_conf = float(min(0.92, phase_conf))
|
||||
|
||||
# —— Event candidates(仅 Spring / SOS / LPS / UTAD)——
|
||||
candidates: List[Dict[str, Any]] = []
|
||||
|
||||
def _add(typ: str, conf: float, note: str) -> None:
|
||||
if typ in confirmed_types:
|
||||
return # 已确认则不再作为 candidate
|
||||
candidates.append(
|
||||
{
|
||||
"type": typ,
|
||||
"confidence": round(float(min(0.9, conf)), 3),
|
||||
"confirmed": False,
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
|
||||
# Spring candidate:刺破或贴近下沿,收盘收回,但未达 Confirmed 规则(或不在 confirmed)
|
||||
pierce_lo = l < lo - tol * 0.15
|
||||
close_back = c >= lo - tol * 0.5
|
||||
if pierce_lo and close_back:
|
||||
_add("Spring", 0.5 + (0.12 if vol_ratio <= 1.2 else 0) + (0.08 if close_back else 0), "假破下沿收回(未确认)")
|
||||
elif l <= lo + tol * 0.35 and close_back and lower_wick >= rng * 0.4:
|
||||
_add("Spring", 0.45 + (0.1 if vol_contract else 0), "测下沿长下影(未确认)")
|
||||
|
||||
# UTAD candidate
|
||||
pierce_hi = h > hi + tol * 0.15
|
||||
close_back_dn = c <= hi + tol * 0.5
|
||||
if pierce_hi and close_back_dn:
|
||||
_add("UTAD", 0.5 + (0.1 if vol_ratio >= 0.9 else 0), "假破上沿跌回(未确认)")
|
||||
|
||||
# SOS candidate:接近/轻破上沿,量能一般,未确认
|
||||
if c > hi - tol * 0.4 or h >= hi:
|
||||
sos_conf = 0.48 + (0.12 if c > hi else 0) + (0.1 if vol_ratio >= 1.05 else 0)
|
||||
_add("SOS", sos_conf, "上破/逼近箱顶(未确认)")
|
||||
|
||||
# LPS candidate:站上 mid/上沿带后回踩
|
||||
if c >= mid and l >= mid - tol * 1.5 and l > lo + (hi - lo) * 0.25:
|
||||
_add("LPS", 0.46 + (0.1 if vol_ratio <= 1.0 else 0), "箱内上沿带回踩(未确认)")
|
||||
|
||||
candidates.sort(key=lambda x: x["confidence"], reverse=True)
|
||||
out["event_candidates"] = candidates[:4]
|
||||
|
||||
event_score = float(candidates[0]["confidence"]) if candidates else 0.25
|
||||
|
||||
# next_expected(简规则)
|
||||
next_exp = None
|
||||
if "Spring" in confirmed_types and "SOS" not in confirmed_types:
|
||||
next_exp = "SOS"
|
||||
elif "SOS" in confirmed_types and "LPS" not in confirmed_types:
|
||||
next_exp = "LPS"
|
||||
elif "UTAD" in confirmed_types and "SOW" not in confirmed_types:
|
||||
next_exp = "SOW"
|
||||
elif any(c["type"] == "Spring" for c in candidates):
|
||||
next_exp = "Test"
|
||||
elif any(c["type"] == "SOS" for c in candidates):
|
||||
next_exp = "LPS"
|
||||
out["next_expected"] = next_exp
|
||||
|
||||
# —— lifecycle ——
|
||||
key_confirmed = confirmed_types & {"Spring", "SOS", "UTAD", "SOW", "LPS", "LPSY"}
|
||||
if key_confirmed:
|
||||
lifecycle = "CONFIRMED"
|
||||
elif range_ok or phase_candidate or candidates:
|
||||
lifecycle = "FORMING"
|
||||
else:
|
||||
lifecycle = "UNKNOWN"
|
||||
out["lifecycle"] = lifecycle
|
||||
|
||||
cycle_c = structure_score
|
||||
overall = 0.35 * cycle_c + 0.25 * phase_conf + 0.25 * event_score + 0.15 * volume_score
|
||||
out["confidence"] = {
|
||||
"cycle": round(cycle_c, 3),
|
||||
"phase": round(phase_conf, 3),
|
||||
"event": round(event_score, 3),
|
||||
"structure": round(structure_score, 3),
|
||||
"volume": round(volume_score, 3),
|
||||
"overall": round(float(overall), 3),
|
||||
}
|
||||
parts = []
|
||||
if out["range_formation"]["potential_trading_range"]:
|
||||
parts.append("Potential TR")
|
||||
if phase_candidate:
|
||||
parts.append(f"Phase {phase_candidate} candidate")
|
||||
if candidates:
|
||||
parts.append(f"{candidates[0]['type']} candidate")
|
||||
out["note"] = "; ".join(parts) if parts else "observing"
|
||||
return out
|
||||
|
||||
|
||||
def execution_signal_from_wyckoff(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Execution 边界:只允许 Confirmed。
|
||||
返回 source='confirmed' 的信号描述;Live-only 时返回 None。
|
||||
"""
|
||||
if not payload:
|
||||
return None
|
||||
cycles = payload.get("cycles") or []
|
||||
active = cycles[0] if cycles else None
|
||||
events = []
|
||||
if active and isinstance(active.get("confirmed"), dict):
|
||||
events = list(active["confirmed"].get("events") or [])
|
||||
if not events:
|
||||
# 兼容旧顶层 events(均为 confirmed 镜像)
|
||||
events = list(payload.get("events") or [])
|
||||
if not events:
|
||||
return None
|
||||
last = events[-1]
|
||||
return {
|
||||
"source": "confirmed",
|
||||
"type": last.get("type"),
|
||||
"time": last.get("time"),
|
||||
"lifecycle": (active or {}).get("lifecycle") or "CONFIRMED",
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
"""交易区间检测:仅负责 TradingRange(起止/高低/结构分)。
|
||||
|
||||
WYCKOFF-MULTI-CYCLE-001:Phase/Event/VP 不得进入本模块。
|
||||
过滤顺序固定:detect → quality → trend → overlap(<0.2) → accept → mask。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
MAX_CYCLES = 8
|
||||
OVERLAP_RATIO_MAX = 0.2
|
||||
|
||||
|
||||
def _atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||||
high = df["high"].astype(float)
|
||||
low = df["low"].astype(float)
|
||||
close = df["close"].astype(float)
|
||||
prev_close = close.shift(1)
|
||||
tr = pd.concat(
|
||||
[
|
||||
(high - low).abs(),
|
||||
(high - prev_close).abs(),
|
||||
(low - prev_close).abs(),
|
||||
],
|
||||
axis=1,
|
||||
).max(axis=1)
|
||||
return tr.rolling(period, min_periods=max(3, period // 2)).mean()
|
||||
|
||||
|
||||
def _robust_width(seg: pd.DataFrame) -> float:
|
||||
"""用 90/10 分位估宽,避免单根影线把长窗卡死。"""
|
||||
h = seg["high"].astype(float)
|
||||
l = seg["low"].astype(float)
|
||||
if len(seg) < 6:
|
||||
return float(h.max() - l.min())
|
||||
return float(np.nanpercentile(h, 90) - np.nanpercentile(l, 10))
|
||||
|
||||
|
||||
def _score_segment(
|
||||
length: int,
|
||||
near_hi: int,
|
||||
near_lo: int,
|
||||
inside: float,
|
||||
width: float,
|
||||
atr: float,
|
||||
) -> float:
|
||||
"""结构质量分(非 Phase/Event)。"""
|
||||
touch = min(near_hi, 6) + min(near_lo, 6)
|
||||
width_pen = (width / atr) if atr > 0 else width
|
||||
return float(touch) * 4.0 + float(inside) * 25.0 - width_pen * 3.0 + min(length / 40.0, 2.0)
|
||||
|
||||
|
||||
def _time_col(df: pd.DataFrame) -> Optional[str]:
|
||||
if "date" in df.columns:
|
||||
return "date"
|
||||
if "timestamp" in df.columns:
|
||||
return "timestamp"
|
||||
return None
|
||||
|
||||
|
||||
def _bar_index_at_or_after(work: pd.DataFrame, ts: Any) -> Optional[int]:
|
||||
col = _time_col(work)
|
||||
if col is None or ts is None:
|
||||
return None
|
||||
try:
|
||||
target = pd.Timestamp(ts)
|
||||
except Exception:
|
||||
return None
|
||||
series = pd.to_datetime(work[col], utc=True, errors="coerce")
|
||||
if target.tzinfo is None:
|
||||
target = target.tz_localize("UTC")
|
||||
else:
|
||||
target = target.tz_convert("UTC")
|
||||
if series.isna().all():
|
||||
return None
|
||||
ge = series >= target
|
||||
if ge.any():
|
||||
return int(np.flatnonzero(ge.to_numpy())[0])
|
||||
return 0
|
||||
|
||||
|
||||
def _pack_range(
|
||||
work: pd.DataFrame,
|
||||
df: pd.DataFrame,
|
||||
start_i: int,
|
||||
end_i: int,
|
||||
hi: float,
|
||||
lo: float,
|
||||
tol: float,
|
||||
last_atr: float,
|
||||
score: float,
|
||||
n: int,
|
||||
window_offset: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
"""组装 TradingRange(仅结构字段)。"""
|
||||
mid = (hi + lo) / 2.0
|
||||
last_c = float(work["close"].iloc[min(end_i, len(work) - 1)])
|
||||
price_in_box = (lo - tol * 1.5) <= last_c <= (hi + tol * 1.5)
|
||||
bars = int(end_i - start_i + 1)
|
||||
# 结构置信:归一化 score(启发式)
|
||||
range_conf = float(np.clip(score / 55.0, 0.05, 0.99))
|
||||
best = {
|
||||
"start_idx": int(start_i),
|
||||
"end_idx": int(end_i),
|
||||
"high": float(hi),
|
||||
"low": float(lo),
|
||||
"mid": float(mid),
|
||||
"active": bool(price_in_box),
|
||||
"atr": float(last_atr),
|
||||
"tol": float(tol),
|
||||
"bars": bars,
|
||||
"score": float(score),
|
||||
"quality": float(score),
|
||||
"range_confidence": range_conf,
|
||||
}
|
||||
|
||||
def _ts(row) -> Any:
|
||||
col = _time_col(work)
|
||||
if col and pd.notna(row[col]):
|
||||
return row[col]
|
||||
return None
|
||||
|
||||
best["start_time"] = _ts(work.iloc[best["start_idx"]])
|
||||
best["end_time"] = _ts(work.iloc[best["end_idx"]])
|
||||
# window_offset:slice 相对父 DataFrame 的起点;勿用 len(df)-len(work)
|
||||
offset = int(window_offset)
|
||||
best["abs_start_idx"] = offset + best["start_idx"]
|
||||
best["abs_end_idx"] = offset + best["end_idx"]
|
||||
best["abs_scan_end_idx"] = offset + n - 1
|
||||
return best
|
||||
|
||||
|
||||
def _overlap_ratio(a0: int, a1: int, b0: int, b1: int) -> float:
|
||||
"""两闭区间重叠长度 / 较短区间长度。"""
|
||||
lo = max(a0, b0)
|
||||
hi = min(a1, b1)
|
||||
if hi < lo:
|
||||
return 0.0
|
||||
overlap = hi - lo + 1
|
||||
shorter = min(a1 - a0 + 1, b1 - b0 + 1)
|
||||
if shorter <= 0:
|
||||
return 0.0
|
||||
return float(overlap) / float(shorter)
|
||||
|
||||
|
||||
def _passes_quality(tr: Dict[str, Any], min_bars: int) -> bool:
|
||||
if tr is None:
|
||||
return False
|
||||
if int(tr.get("bars") or 0) < max(8, min_bars // 2):
|
||||
return False
|
||||
if float(tr.get("score") or 0) < 12.0:
|
||||
return False
|
||||
hi = float(tr["high"])
|
||||
lo = float(tr["low"])
|
||||
atr = float(tr.get("atr") or 0) or 1.0
|
||||
if (hi - lo) / atr > 12.0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _passes_trend_filter(work: pd.DataFrame, tr: Dict[str, Any]) -> bool:
|
||||
"""趋势污染:定向位移过大则非震荡箱。"""
|
||||
s = int(tr["start_idx"])
|
||||
e = int(tr["end_idx"])
|
||||
seg = work.iloc[s : e + 1]
|
||||
if len(seg) < 8:
|
||||
return False
|
||||
c0 = float(seg["close"].iloc[0])
|
||||
c1 = float(seg["close"].iloc[-1])
|
||||
atr = float(tr.get("atr") or 0) or 1.0
|
||||
drift = abs(c1 - c0) / atr
|
||||
# 相对箱宽:漂移占箱宽过大 → 趋势
|
||||
width = max(float(tr["high"]) - float(tr["low"]), atr)
|
||||
drift_frac = abs(c1 - c0) / width
|
||||
if drift > 6.0 and drift_frac > 0.55:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _detect_in_window(
|
||||
df: pd.DataFrame,
|
||||
win_start: int,
|
||||
win_end: int,
|
||||
min_bars: int = 24,
|
||||
atr_mult: float = 1.2,
|
||||
tail_reserve: int = 12,
|
||||
prefer_start_time: Any = None,
|
||||
range_start_time: Any = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
在 df[win_start:win_end+1] 内检测单个 TradingRange。
|
||||
只返回箱体结构,不含 Phase/Event/VP。
|
||||
"""
|
||||
if df is None or win_end < win_start:
|
||||
return None
|
||||
slice_df = df.iloc[win_start : win_end + 1].reset_index(drop=True)
|
||||
lookback = len(slice_df)
|
||||
if lookback < min_bars + 5:
|
||||
return None
|
||||
|
||||
work = slice_df
|
||||
n = len(work)
|
||||
reserve = min(tail_reserve, max(0, n - min_bars - 2))
|
||||
core_end = n - reserve if reserve > 0 else n
|
||||
core = work.iloc[:core_end]
|
||||
if len(core) < min_bars:
|
||||
core = work
|
||||
core_end = n
|
||||
reserve = 0
|
||||
|
||||
atr = _atr(work)
|
||||
last_atr = float(atr.iloc[core_end - 1]) if atr.notna().iloc[:core_end].any() else float(
|
||||
(core["high"] - core["low"]).mean()
|
||||
)
|
||||
if not np.isfinite(last_atr) or last_atr <= 0:
|
||||
last_atr = float(core["close"].iloc[-1]) * 0.01
|
||||
|
||||
eff_atr_mult = float(atr_mult)
|
||||
if lookback >= 280:
|
||||
eff_atr_mult = atr_mult * 1.7
|
||||
elif lookback >= 160:
|
||||
eff_atr_mult = atr_mult * 1.3
|
||||
width_factor = 3.8 + min(2.2, max(0.0, (lookback - 80) / 100.0))
|
||||
max_width = last_atr * eff_atr_mult * width_factor
|
||||
tol = last_atr * eff_atr_mult * 0.35
|
||||
|
||||
prefer_i = None
|
||||
if prefer_start_time is not None:
|
||||
prefer_i = _bar_index_at_or_after(work, prefer_start_time)
|
||||
|
||||
if range_start_time is not None:
|
||||
start_i = _bar_index_at_or_after(work, range_start_time)
|
||||
if start_i is not None and start_i <= core_end - 8:
|
||||
seg = work.iloc[start_i:core_end]
|
||||
hi = float(seg["high"].max())
|
||||
lo = float(seg["low"].min())
|
||||
rw = _robust_width(seg)
|
||||
if 0 < rw <= max_width * 1.15:
|
||||
near_hi = int((seg["high"] >= hi - tol).sum())
|
||||
near_lo = int((seg["low"] <= lo + tol).sum())
|
||||
inside = float(((seg["close"] >= lo - tol) & (seg["close"] <= hi + tol)).mean())
|
||||
if near_hi >= 2 and near_lo >= 2 and inside >= 0.70:
|
||||
score = _score_segment(len(seg), near_hi, near_lo, inside, rw, last_atr)
|
||||
return _pack_range(
|
||||
work, df, start_i, core_end - 1, hi, lo, tol, last_atr, score, n,
|
||||
window_offset=win_start,
|
||||
)
|
||||
|
||||
eff_min_bars = max(8, int(min_bars))
|
||||
cn = len(core)
|
||||
max_bars = min(cn, max(eff_min_bars * 2, min(96, max(eff_min_bars + 8, int(cn * 0.5)))))
|
||||
cands: List[Tuple[float, int, int, int, float, float, float]] = []
|
||||
|
||||
def _try_seg(start_i: int, end_i: int, prefer_boost: float = 0.0) -> None:
|
||||
if end_i - start_i + 1 < eff_min_bars:
|
||||
return
|
||||
if start_i < 0 or end_i >= cn or start_i > end_i:
|
||||
return
|
||||
seg = work.iloc[start_i : end_i + 1]
|
||||
hi = float(seg["high"].max())
|
||||
lo = float(seg["low"].min())
|
||||
rw = _robust_width(seg)
|
||||
if rw <= 0 or rw > max_width:
|
||||
return
|
||||
raw_w = hi - lo
|
||||
if raw_w > max_width * 1.35:
|
||||
return
|
||||
near_hi = int((seg["high"] >= hi - tol).sum())
|
||||
near_lo = int((seg["low"] <= lo + tol).sum())
|
||||
if near_hi < 2 or near_lo < 2:
|
||||
return
|
||||
inside = float(((seg["close"] >= lo - tol) & (seg["close"] <= hi + tol)).mean())
|
||||
if inside < 0.72:
|
||||
return
|
||||
length = end_i - start_i + 1
|
||||
score = _score_segment(length, near_hi, near_lo, inside, rw, last_atr) + prefer_boost
|
||||
cands.append((score, length, start_i, end_i, hi, lo, rw))
|
||||
|
||||
for length in range(min(cn, max_bars), eff_min_bars - 1, -4):
|
||||
start_i = cn - length
|
||||
boost = 0.0
|
||||
if prefer_i is not None:
|
||||
dist = abs(start_i - int(prefer_i))
|
||||
if dist <= 6:
|
||||
boost = 10.0
|
||||
elif dist <= 14:
|
||||
boost = 4.0
|
||||
elif start_i > int(prefer_i) + 16:
|
||||
boost = -10.0
|
||||
_try_seg(start_i, cn - 1, boost)
|
||||
|
||||
if prefer_i is not None:
|
||||
pi = int(prefer_i)
|
||||
if 0 <= pi < cn:
|
||||
align_max = min(cn, max(max_bars, int(cn * 0.65)))
|
||||
alen = cn - pi
|
||||
if eff_min_bars <= alen <= align_max:
|
||||
_try_seg(pi, cn - 1, prefer_boost=18.0)
|
||||
elif alen > align_max:
|
||||
start_i = max(0, cn - align_max)
|
||||
if start_i > pi:
|
||||
start_i = pi
|
||||
end_i = min(cn - 1, pi + align_max - 1)
|
||||
else:
|
||||
end_i = cn - 1
|
||||
_try_seg(start_i, end_i, prefer_boost=12.0)
|
||||
|
||||
if not cands:
|
||||
return None
|
||||
|
||||
cands.sort(key=lambda x: x[0], reverse=True)
|
||||
best_score = cands[0][0]
|
||||
band = max(4.0, abs(best_score) * 0.10)
|
||||
near = [c for c in cands if c[0] >= best_score - band]
|
||||
chosen = max(near, key=lambda x: (x[1], x[0]))
|
||||
score, _length, start_i, end_i, hi, lo, _rw = chosen
|
||||
return _pack_range(work, df, start_i, end_i, hi, lo, tol, last_atr, score, n, window_offset=win_start)
|
||||
|
||||
|
||||
def detect_trading_ranges(
|
||||
df: pd.DataFrame,
|
||||
lookback: Optional[int] = None,
|
||||
min_bars: int = 24,
|
||||
atr_mult: float = 1.2,
|
||||
tail_reserve: int = 12,
|
||||
max_cycles: int = MAX_CYCLES,
|
||||
prefer_start_time: Any = None,
|
||||
range_start_time: Any = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
倒序切多段 TradingRange(近→远)。
|
||||
过滤顺序:detect → quality → trend → overlap → accept → mask。
|
||||
返回列表已按时间倒序,调用方将 [0] 标为 ACTIVE。
|
||||
"""
|
||||
if df is None or len(df) < min_bars + 5:
|
||||
return []
|
||||
lb = int(lookback) if lookback is not None else len(df)
|
||||
work = df.tail(lb).reset_index(drop=True)
|
||||
n = len(work)
|
||||
occupied: List[Dict[str, Any]] = []
|
||||
accepted: List[Dict[str, Any]] = []
|
||||
|
||||
# 搜索右端从 n-1 往左收缩;每接受一段后右端移到该段 start 之前
|
||||
search_end = n - 1
|
||||
prefer = prefer_start_time
|
||||
hard_start = range_start_time
|
||||
|
||||
while len(accepted) < max(1, int(max_cycles)) and search_end >= min_bars + 4:
|
||||
# 在剩余历史内从右往左试多个右边界,避免历史箱必须贴住 search_end
|
||||
# (否则中间趋势会挡住更早的真实箱)
|
||||
cand = None
|
||||
step = max(4, min(12, (search_end - min_bars) // 10 or 4))
|
||||
for end_try in range(search_end, min_bars + 4, -step):
|
||||
trial = _detect_in_window(
|
||||
work,
|
||||
0,
|
||||
end_try,
|
||||
min_bars=min_bars,
|
||||
atr_mult=atr_mult,
|
||||
tail_reserve=tail_reserve,
|
||||
prefer_start_time=prefer if len(accepted) == 0 and end_try == search_end else None,
|
||||
range_start_time=hard_start if len(accepted) == 0 and end_try == search_end else None,
|
||||
)
|
||||
# 1) detect
|
||||
if trial is None:
|
||||
continue
|
||||
# 2) quality
|
||||
if not _passes_quality(trial, min_bars):
|
||||
continue
|
||||
# 3) trend contamination
|
||||
if not _passes_trend_filter(work, trial):
|
||||
continue
|
||||
# 4) overlap with accepted
|
||||
a0, a1 = int(trial["abs_start_idx"]), int(trial["abs_end_idx"])
|
||||
overlap_bad = False
|
||||
for occ in occupied:
|
||||
ratio = _overlap_ratio(a0, a1, int(occ["start"]), int(occ["end"]))
|
||||
if ratio >= OVERLAP_RATIO_MAX:
|
||||
overlap_bad = True
|
||||
break
|
||||
if overlap_bad:
|
||||
continue
|
||||
# 取最靠右的合格箱(倒序第一段)
|
||||
cand = trial
|
||||
break
|
||||
|
||||
if cand is None:
|
||||
break
|
||||
|
||||
# 5) accept
|
||||
accepted.append(cand)
|
||||
a0, a1 = int(cand["abs_start_idx"]), int(cand["abs_end_idx"])
|
||||
# 6) mask
|
||||
occupied.append(
|
||||
{
|
||||
"start": a0,
|
||||
"end": max(a1, int(cand.get("abs_scan_end_idx", a1))),
|
||||
"quality": float(cand.get("quality") or 0),
|
||||
"high": float(cand["high"]),
|
||||
"low": float(cand["low"]),
|
||||
}
|
||||
)
|
||||
# 下一轮只在更早窗口搜
|
||||
search_end = int(cand["abs_start_idx"]) - 1
|
||||
hard_start = None
|
||||
prefer = None
|
||||
|
||||
# abs_* 目前相对 work;若 df 比 work 长需加 offset
|
||||
offset = len(df) - len(work)
|
||||
if offset:
|
||||
for tr in accepted:
|
||||
tr["abs_start_idx"] = int(tr["abs_start_idx"]) + offset
|
||||
tr["abs_end_idx"] = int(tr["abs_end_idx"]) + offset
|
||||
tr["abs_scan_end_idx"] = int(tr["abs_scan_end_idx"]) + offset
|
||||
|
||||
return accepted
|
||||
|
||||
|
||||
def detect_trading_range(
|
||||
df: pd.DataFrame,
|
||||
lookback: int = 120,
|
||||
min_bars: int = 24,
|
||||
atr_mult: float = 1.2,
|
||||
tail_reserve: int = 12,
|
||||
range_start_time: Any = None,
|
||||
prefer_start_time: Any = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""兼容旧接口:返回倒序列表中的第一段(ACTIVE 候选)。"""
|
||||
ranges = detect_trading_ranges(
|
||||
df,
|
||||
lookback=lookback,
|
||||
min_bars=min_bars,
|
||||
atr_mult=atr_mult,
|
||||
tail_reserve=tail_reserve,
|
||||
max_cycles=1,
|
||||
prefer_start_time=prefer_start_time,
|
||||
range_start_time=range_start_time,
|
||||
)
|
||||
return ranges[0] if ranges else None
|
||||
@@ -0,0 +1,72 @@
|
||||
"""区间内 Volume Profile。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def compute_volume_profile(
|
||||
df: pd.DataFrame,
|
||||
start_idx: int,
|
||||
end_idx: int,
|
||||
bin_count: int = 50,
|
||||
value_area_pct: float = 0.70,
|
||||
) -> Dict[str, Any]:
|
||||
seg = df.iloc[start_idx : end_idx + 1]
|
||||
if seg.empty:
|
||||
return {"bins": [], "poc": None, "vah": None, "val": None, "bin_count": bin_count}
|
||||
|
||||
typical = (seg["high"].astype(float) + seg["low"].astype(float) + seg["close"].astype(float)) / 3.0
|
||||
vol = seg["volume"].astype(float).fillna(0.0)
|
||||
lo = float(seg["low"].min())
|
||||
hi = float(seg["high"].max())
|
||||
if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:
|
||||
mid = float(seg["close"].iloc[-1])
|
||||
return {
|
||||
"bins": [{"price": mid, "volume": float(vol.sum())}],
|
||||
"poc": mid,
|
||||
"vah": mid,
|
||||
"val": mid,
|
||||
"bin_count": 1,
|
||||
}
|
||||
|
||||
edges = np.linspace(lo, hi, bin_count + 1)
|
||||
# 右开最后一桶闭合
|
||||
idx = np.clip(np.digitize(typical.values, edges) - 1, 0, bin_count - 1)
|
||||
vols = np.zeros(bin_count, dtype=float)
|
||||
for i, v in zip(idx, vol.values):
|
||||
vols[i] += float(v)
|
||||
|
||||
centers = (edges[:-1] + edges[1:]) / 2.0
|
||||
poc_i = int(np.argmax(vols)) if vols.sum() > 0 else bin_count // 2
|
||||
poc = float(centers[poc_i])
|
||||
|
||||
# Value Area:从 POC 向两侧扩展直到累计 >= value_area_pct
|
||||
total = float(vols.sum()) or 1.0
|
||||
target = total * value_area_pct
|
||||
left = right = poc_i
|
||||
acc = float(vols[poc_i])
|
||||
while acc < target and (left > 0 or right < bin_count - 1):
|
||||
left_v = vols[left - 1] if left > 0 else -1.0
|
||||
right_v = vols[right + 1] if right < bin_count - 1 else -1.0
|
||||
if right_v >= left_v and right < bin_count - 1:
|
||||
right += 1
|
||||
acc += float(vols[right])
|
||||
elif left > 0:
|
||||
left -= 1
|
||||
acc += float(vols[left])
|
||||
else:
|
||||
break
|
||||
|
||||
bins: List[Dict[str, float]] = [
|
||||
{"price": float(centers[i]), "volume": float(vols[i])} for i in range(bin_count)
|
||||
]
|
||||
return {
|
||||
"bins": bins,
|
||||
"poc": poc,
|
||||
"vah": float(centers[right]),
|
||||
"val": float(centers[left]),
|
||||
"bin_count": bin_count,
|
||||
}
|
||||
@@ -281,10 +281,6 @@ class Chan_BSP_TYPE(Enum):
|
||||
S1 = auto()
|
||||
S2 = auto()
|
||||
S3 = auto()
|
||||
# 第四类:三类买卖点的低滞后变体,几何位置同 B3/S3,但不等笔确认。
|
||||
# 见 chanlun/analysis/fast_bsp.py
|
||||
B4 = auto()
|
||||
S4 = auto()
|
||||
NONE = auto()
|
||||
"""
|
||||
class Chan_BSP_TYPE(Enum):
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from chanlun.core.ChanEnum import Chan_BSP_DIR, Chan_BSP_TYPE
|
||||
|
||||
|
||||
class ChanFastBSP():
|
||||
"""第四类买卖点(B4/S4)。
|
||||
|
||||
与 ChanBSP 的区别在于它不挂在笔上:fast_bsp 刻意不等笔确认,入场点是一根具体的
|
||||
K线而非一笔的端点,所以时间与价格直接取自 K 线,没有 bi / klc 可依附。
|
||||
|
||||
htf_agree 与 ladder_ok 是两个独立的过滤标志,不在这里合成——上层(图表或策略)
|
||||
自己决定要不要用、怎么组合。
|
||||
"""
|
||||
|
||||
def __init__(self, time, price, ddir, entry_idx, bo_time=None, pb_time=None,
|
||||
lag=0, depth=0.0, zg=None, zd=None, occ=1,
|
||||
htf_dir=None, htf_agree=None, ladder_ok=None):
|
||||
self.time = time
|
||||
self.price = float(price)
|
||||
self.dir = ddir
|
||||
self.type = Chan_BSP_TYPE.B4 if ddir == Chan_BSP_DIR.BUY else Chan_BSP_TYPE.S4
|
||||
self.entry_idx = int(entry_idx)
|
||||
self.bo_time = bo_time
|
||||
self.pb_time = pb_time
|
||||
self.lag = int(lag)
|
||||
self.depth = float(depth)
|
||||
self.zg = float(zg) if zg is not None else None
|
||||
self.zd = float(zd) if zd is not None else None
|
||||
self.occ = int(occ)
|
||||
self.htf_dir = htf_dir
|
||||
self.htf_agree = htf_agree
|
||||
self.ladder_ok = ladder_ok
|
||||
# 入场即成立,没有「等待确认」这个状态;留此字段是为了与 ChanBSP 的序列化对齐
|
||||
self.is_sure = True
|
||||
self.start_time = time
|
||||
self.end_time = time
|
||||
self.sure_time = time
|
||||
|
||||
def __repr__(self):
|
||||
name = str(self.type).replace('Chan_BSP_TYPE.', '')
|
||||
return f"<ChanFastBSP {name} {self.time} {self.price} lag={self.lag}>"
|
||||
+8
-32
@@ -63,11 +63,10 @@ class ChanKLC():
|
||||
self.bsp = False
|
||||
self.bsp_type = Chan_BSP_TYPE.NONE
|
||||
# EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC}
|
||||
self._ema_status = {}
|
||||
self._ema_status_dirty = False
|
||||
self.ema_status = {}
|
||||
# 向后兼容:保留 ema52_status 和 ema52_pos
|
||||
self._ema52_status = 0
|
||||
self._ema52_pos = Chan_EMA_POS.UNKNOWN
|
||||
self.ema52_status = 0
|
||||
self.ema52_pos = Chan_EMA_POS.UNKNOWN
|
||||
self.bb2633upper = klu.bb2633upper
|
||||
self.bb2633lower = klu.bb2633lower
|
||||
self.bb2633middle = klu.bb2633middle
|
||||
@@ -284,44 +283,21 @@ class ChanKLC():
|
||||
'ema156': self.ema156,
|
||||
'ema208': self.ema208,
|
||||
}
|
||||
self._ema_status_dirty = False
|
||||
self._ema_status = {}
|
||||
self.ema_status = {}
|
||||
for name, value in ema_configs.items():
|
||||
# 按 EMA 值的百分比自动计算阈值
|
||||
threshold = abs(value) * self.threshold_pct if value and self.threshold_pct > 0 else 0
|
||||
pos = ChanKLC.cal_ema_pos(self.high, self.low, self.close, value, threshold)
|
||||
semantic = ChanKLC.cal_ema_semantic(pos, self.dir, self.ema_dir)
|
||||
self._ema_status[name] = {
|
||||
self.ema_status[name] = {
|
||||
'pos': pos,
|
||||
'semantic': semantic,
|
||||
'value': value,
|
||||
'threshold': threshold,
|
||||
}
|
||||
# 向后兼容
|
||||
self._ema52_pos = self._ema_status['ema52']['pos']
|
||||
self._ema52_status = ChanKLC.semantic_to_int(self._ema_status['ema52']['semantic'])
|
||||
|
||||
# 以下三个改成惰性求值。原来 set_end_klu 每次合并 KLU 都会立刻重算一遍,
|
||||
# 实测占 TF_DF 构建的约 25%,而全仓(含前端)没有任何地方读取它的产出。
|
||||
# 保留属性形式是为了任何外部读取仍拿到正确值,只是推迟到真被读时才算。
|
||||
@property
|
||||
def ema_status(self):
|
||||
if self._ema_status_dirty:
|
||||
self.cal_all_ema_status()
|
||||
return self._ema_status
|
||||
|
||||
@property
|
||||
def ema52_pos(self):
|
||||
if self._ema_status_dirty:
|
||||
self.cal_all_ema_status()
|
||||
return self._ema52_pos
|
||||
|
||||
@property
|
||||
def ema52_status(self):
|
||||
if self._ema_status_dirty:
|
||||
self.cal_all_ema_status()
|
||||
return self._ema52_status
|
||||
|
||||
self.ema52_pos = self.ema_status['ema52']['pos']
|
||||
self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic'])
|
||||
def get_ema_pos(self, ema_name):
|
||||
"""获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')"""
|
||||
if ema_name in self.ema_status:
|
||||
@@ -472,7 +448,7 @@ class ChanKLC():
|
||||
klu.set_klc(self)
|
||||
self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN
|
||||
self.cal_indicators()
|
||||
self._ema_status_dirty = True
|
||||
self.cal_all_ema_status()
|
||||
if self.open > self.high:
|
||||
self.open = self.high
|
||||
if self.close > self.high:
|
||||
|
||||
+20
-33
@@ -160,40 +160,27 @@ class ChanKLU:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
# 属性名 ← dataframe 列名。两条赋值路径(逐行 Series / 预取 ndarray)共用这张表,
|
||||
# 免得将来加指标时只改一处、另一处静默漏掉。
|
||||
INDICATOR_FIELDS = (
|
||||
('macd', 'macd'), ('signal', 'macdsignal'), ('macdhist', 'macdhist'),
|
||||
('ema26', 'ema26'), ('ema52', 'ema52'), ('ema24', 'ema24'),
|
||||
('ema104', 'ema104'), ('ema156', 'ema156'), ('ema208', 'ema208'),
|
||||
('ema13', 'ema13'), ('ema7', 'ema7'), ('ema5', 'ema5'),
|
||||
('rsi', 'rsi'), ('volume_ratio', 'volume_ratio'),
|
||||
('bb52upper', 'bb52upper'), ('bb52lower', 'bb52lower'),
|
||||
('bb2633upper', 'bb2633upper'), ('bb2633lower', 'bb2633lower'),
|
||||
('bb2633middle', 'bb2633middle'), ('ma5', 'ma5'),
|
||||
)
|
||||
|
||||
def set_indicators(self, item):
|
||||
"""单根赋值。增量追加时每次只有一根,走这条即可。
|
||||
|
||||
原写法是 `float(item[c]) if c in item and item[c] else 0`。其中的真值判断
|
||||
是空转:值为 0.0 时 float(0.0) 仍是 0,值为 NaN 时 NaN 为真值、照样透传。
|
||||
唯一起作用的是「列不存在则填 0」,所以这里只保留那一层。
|
||||
"""
|
||||
for attr, col in self.INDICATOR_FIELDS:
|
||||
v = item[col] if col in item else 0
|
||||
setattr(self, attr, float(v) if v else 0)
|
||||
|
||||
def set_indicators_from(self, cols, i):
|
||||
"""从预取的 {列名: ndarray} 按下标赋值,语义与 set_indicators 相同。
|
||||
|
||||
全量构建时用这条:避免每根 `df.iloc[i]` 构造一个 Series,再在其上做
|
||||
几十次逐键查找——那是 TF_DF 构建 96% 的耗时所在。
|
||||
"""
|
||||
for attr, col in self.INDICATOR_FIELDS:
|
||||
arr = cols.get(col)
|
||||
v = arr[i] if arr is not None else 0
|
||||
setattr(self, attr, float(v) if v else 0)
|
||||
self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0
|
||||
self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0
|
||||
self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0
|
||||
self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0
|
||||
self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0
|
||||
self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0
|
||||
self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0
|
||||
self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0
|
||||
self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0
|
||||
self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0
|
||||
self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0
|
||||
self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0
|
||||
self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0
|
||||
self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0
|
||||
self.bb52lower = float(item['bb52lower']) if 'bb52lower' in item and item['bb52lower'] else 0
|
||||
self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0
|
||||
self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0
|
||||
self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0
|
||||
self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0
|
||||
self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0
|
||||
def cal_macd_state(self):
|
||||
# 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN
|
||||
# 首条或缺前一根
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
"""Drop-in replacement for the `talib.abstract` calls this project makes.
|
||||
|
||||
Same call signatures, same column names, same NaN warm-up lengths, so call
|
||||
sites only change their import line.
|
||||
|
||||
Only what the codebase actually uses is implemented: SMA, MA, EMA, RSI, ATR,
|
||||
MACD, BBANDS. Numerical agreement with TA-Lib is enforced by
|
||||
`chanlun/tests/test_ta_compat.py`, which skips when talib is absent.
|
||||
|
||||
The warm-up conventions below are TA-Lib's, not the textbook ones, and they
|
||||
differ between functions — getting them wrong shifts every downstream Chan
|
||||
structure by a bar:
|
||||
|
||||
SMA/BBANDS first value at index period-1
|
||||
EMA seeded with the SMA of the first `period` values, at index period-1
|
||||
RSI/ATR Wilder smoothing (alpha = 1/period), first value at index period
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
__all__ = ["SMA", "MA", "EMA", "RSI", "ATR", "MACD", "BBANDS"]
|
||||
|
||||
|
||||
def _series(data, price: str = "close") -> pd.Series:
|
||||
"""Accept the abstract-API shapes: DataFrame, Series, or ndarray."""
|
||||
if isinstance(data, pd.DataFrame):
|
||||
return data[price].astype(float)
|
||||
if isinstance(data, pd.Series):
|
||||
return data.astype(float)
|
||||
return pd.Series(np.asarray(data, dtype=float))
|
||||
|
||||
|
||||
def _recursive(values: np.ndarray, seed: float, start: int, alpha: float, n: int) -> np.ndarray:
|
||||
"""out[start] = seed; out[i] = alpha*values[i] + (1-alpha)*out[i-1].
|
||||
|
||||
Delegates the recursion to pandas' C implementation rather than a Python
|
||||
loop — `research/` runs this over long histories.
|
||||
"""
|
||||
out = np.full(n, np.nan)
|
||||
if start >= n:
|
||||
return out
|
||||
tail = values[start:].astype(float).copy()
|
||||
tail[0] = seed
|
||||
out[start:] = pd.Series(tail).ewm(alpha=alpha, adjust=False).mean().to_numpy()
|
||||
return out
|
||||
|
||||
|
||||
def SMA(data, timeperiod: int = 30, price: str = "close") -> pd.Series:
|
||||
s = _series(data, price)
|
||||
return s.rolling(window=timeperiod, min_periods=timeperiod).mean()
|
||||
|
||||
|
||||
def MA(data, timeperiod: int = 30, matype: int = 0, price: str = "close") -> pd.Series:
|
||||
if matype != 0:
|
||||
raise NotImplementedError(f"MA matype={matype} is not used by this codebase")
|
||||
return SMA(data, timeperiod, price=price)
|
||||
|
||||
|
||||
def _ema(x: np.ndarray, period: int, start: int) -> np.ndarray:
|
||||
"""EMA whose first output lands on `start`, seeded by the SMA of the
|
||||
`period` values ending there.
|
||||
|
||||
`start` is a parameter because MACD needs the fast EMA to begin later than
|
||||
it naturally would; see the note in MACD().
|
||||
"""
|
||||
n = x.size
|
||||
if n <= start or start < period - 1:
|
||||
return np.full(n, np.nan)
|
||||
seed = x[start - period + 1: start + 1].mean()
|
||||
return _recursive(x, seed, start, 2.0 / (period + 1.0), n)
|
||||
|
||||
|
||||
def EMA(data, timeperiod: int = 30, price: str = "close") -> pd.Series:
|
||||
s = _series(data, price)
|
||||
x = s.to_numpy(dtype=float)
|
||||
return pd.Series(_ema(x, timeperiod, timeperiod - 1), index=s.index)
|
||||
|
||||
|
||||
def RSI(data, timeperiod: int = 14, price: str = "close") -> pd.Series:
|
||||
s = _series(data, price)
|
||||
x = s.to_numpy(dtype=float)
|
||||
n = x.size
|
||||
out = np.full(n, np.nan)
|
||||
if n <= timeperiod:
|
||||
return pd.Series(out, index=s.index)
|
||||
|
||||
delta = np.diff(x)
|
||||
gain = np.where(delta > 0.0, delta, 0.0)
|
||||
loss = np.where(delta < 0.0, -delta, 0.0)
|
||||
|
||||
# delta[k] corresponds to bar k+1, so the first `timeperiod` deltas seed bar `timeperiod`.
|
||||
alpha = 1.0 / timeperiod
|
||||
avg_gain = _recursive(gain, gain[:timeperiod].mean(), timeperiod - 1, alpha, n - 1)
|
||||
avg_loss = _recursive(loss, loss[:timeperiod].mean(), timeperiod - 1, alpha, n - 1)
|
||||
|
||||
ag = avg_gain[timeperiod - 1:]
|
||||
al = avg_loss[timeperiod - 1:]
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
rsi = np.where(al == 0.0, 100.0, 100.0 - 100.0 / (1.0 + ag / al))
|
||||
out[timeperiod:] = rsi
|
||||
return pd.Series(out, index=s.index)
|
||||
|
||||
|
||||
def ATR(data, timeperiod: int = 14) -> pd.Series:
|
||||
if not isinstance(data, pd.DataFrame):
|
||||
raise TypeError("ATR needs a DataFrame with high/low/close")
|
||||
high = data["high"].to_numpy(dtype=float)
|
||||
low = data["low"].to_numpy(dtype=float)
|
||||
close = data["close"].to_numpy(dtype=float)
|
||||
n = high.size
|
||||
out = np.full(n, np.nan)
|
||||
if n <= timeperiod:
|
||||
return pd.Series(out, index=data.index)
|
||||
|
||||
prev_close = close[:-1]
|
||||
tr = np.maximum.reduce([
|
||||
high[1:] - low[1:],
|
||||
np.abs(high[1:] - prev_close),
|
||||
np.abs(low[1:] - prev_close),
|
||||
])
|
||||
|
||||
# tr[k] is bar k+1; the first `timeperiod` true ranges seed bar `timeperiod`.
|
||||
smoothed = _recursive(tr, tr[:timeperiod].mean(), timeperiod - 1, 1.0 / timeperiod, n - 1)
|
||||
out[timeperiod:] = smoothed[timeperiod - 1:]
|
||||
return pd.Series(out, index=data.index)
|
||||
|
||||
|
||||
def MACD(
|
||||
data,
|
||||
fastperiod: int = 12,
|
||||
slowperiod: int = 26,
|
||||
signalperiod: int = 9,
|
||||
price: str = "close",
|
||||
) -> pd.DataFrame:
|
||||
if slowperiod < fastperiod:
|
||||
fastperiod, slowperiod = slowperiod, fastperiod
|
||||
|
||||
s = _series(data, price)
|
||||
x = s.to_numpy(dtype=float)
|
||||
n = x.size
|
||||
|
||||
macd = np.full(n, np.nan)
|
||||
signal = np.full(n, np.nan)
|
||||
hist = np.full(n, np.nan)
|
||||
empty = pd.DataFrame({"macd": macd, "macdsignal": signal, "macdhist": hist}, index=s.index)
|
||||
|
||||
# Both EMAs emit their first value on the same bar. That makes the slow one
|
||||
# ordinary, but re-seeds the fast one from the SMA of the `fastperiod`
|
||||
# values ending there instead of carrying the recursion forward from bar
|
||||
# fastperiod-1 — the two disagree by ~0.2 on a 100-priced series.
|
||||
macd_start = slowperiod - 1
|
||||
if n <= macd_start:
|
||||
return empty
|
||||
line = _ema(x, fastperiod, macd_start) - _ema(x, slowperiod, macd_start)
|
||||
|
||||
# The signal EMA runs over the MACD line, so everything shifts by another
|
||||
# signalperiod-1 bars, and TA-Lib trims the MACD line to match.
|
||||
valid = line[macd_start:]
|
||||
if valid.size < signalperiod:
|
||||
return empty
|
||||
sig = _recursive(
|
||||
valid, valid[:signalperiod].mean(), signalperiod - 1, 2.0 / (signalperiod + 1.0), valid.size
|
||||
)
|
||||
start = macd_start + signalperiod - 1
|
||||
macd[start:] = line[start:]
|
||||
signal[macd_start:] = sig
|
||||
hist = macd - signal
|
||||
|
||||
return pd.DataFrame({"macd": macd, "macdsignal": signal, "macdhist": hist}, index=s.index)
|
||||
|
||||
|
||||
def BBANDS(
|
||||
data,
|
||||
timeperiod: int = 5,
|
||||
nbdevup: float = 2.0,
|
||||
nbdevdn: float = 2.0,
|
||||
matype: int = 0,
|
||||
price: str = "close",
|
||||
) -> pd.DataFrame:
|
||||
if matype != 0:
|
||||
raise NotImplementedError(f"BBANDS matype={matype} is not used by this codebase")
|
||||
s = _series(data, price)
|
||||
middle = s.rolling(window=timeperiod, min_periods=timeperiod).mean()
|
||||
# TA-Lib uses the population standard deviation.
|
||||
std = s.rolling(window=timeperiod, min_periods=timeperiod).std(ddof=0)
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"upperband": middle + nbdevup * std,
|
||||
"middleband": middle,
|
||||
"lowerband": middle - nbdevdn * std,
|
||||
},
|
||||
index=s.index,
|
||||
)
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
@@ -465,7 +467,7 @@ class BiBuilderMixin:
|
||||
pre_last_bi = bi_list[-2]
|
||||
last_bi = bi_list[-1]
|
||||
if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP and False:
|
||||
#pre_last_bi.update_bi(klc)
|
||||
pre_last_bi.update_bi(klc)
|
||||
bi_list.remove(last_bi)
|
||||
pre_last_bi.set_next(None)
|
||||
#last_top.set_fx(Chan_FX_TYPE.PTOP)
|
||||
@@ -583,7 +585,7 @@ class BiBuilderMixin:
|
||||
pre_last_bi = bi_list[-2]
|
||||
last_bi = bi_list[-1]
|
||||
if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN and False:
|
||||
#pre_last_bi.update_bi(klc)
|
||||
pre_last_bi.update_bi(klc)
|
||||
bi_list.remove(last_bi)
|
||||
pre_last_bi.set_next(None)
|
||||
last_bottom = klc
|
||||
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
"""第四类买卖点(B4/S4)接入 TF_DF。
|
||||
|
||||
判定逻辑全在 chanlun/analysis/fast_bsp.py,这里只负责把引擎的中枢/K线喂进去,
|
||||
再把结果包成 ChanFastBSP。
|
||||
|
||||
刻意不在 init_TF_DF 里默认计算:现有构造路径的开销保持不变,由调用方按需触发。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from chanlun.analysis.fast_bsp import (
|
||||
add_zone_ladder,
|
||||
attach_htf_agree,
|
||||
attach_zone_ladder,
|
||||
ensure_timestamp,
|
||||
find_fast_bsp3,
|
||||
htf_fx_timeline,
|
||||
zones_from_zs_list,
|
||||
)
|
||||
from chanlun.core.ChanEnum import Chan_BSP_DIR
|
||||
from chanlun.core.ChanFastBSP import ChanFastBSP
|
||||
|
||||
# 区间套配对:小级别出中枢与买卖点,大级别只出分型定方向。
|
||||
# 取值来自 research/HANDOFF.md §1.5,是回测里实际用过的组合。
|
||||
FAST_BSP_HTF_PAIR = {
|
||||
'1m': '5m',
|
||||
'5m': '30m',
|
||||
'15m': '1h',
|
||||
'30m': '2h',
|
||||
}
|
||||
|
||||
# 未列入配对表的周期回落到这个倍数
|
||||
FAST_BSP_HTF_FALLBACK_RATIO = 4
|
||||
|
||||
_TF_UNIT_MINUTES = {'m': 1, 'h': 60, 'd': 1440, 'w': 10080}
|
||||
|
||||
|
||||
def timeframe_minutes(tf: str) -> int | None:
|
||||
"""'30m' -> 30,'2h' -> 120。无法解析时返回 None。"""
|
||||
if not tf:
|
||||
return None
|
||||
m = re.fullmatch(r'(\d+)\s*([mhdw])', str(tf).strip().lower())
|
||||
if not m:
|
||||
return None
|
||||
return int(m.group(1)) * _TF_UNIT_MINUTES[m.group(2)]
|
||||
|
||||
|
||||
def resolve_htf(tf: str) -> tuple[str, int] | None:
|
||||
"""给小级别找配套的大级别,返回 (标签, 分钟数)。"""
|
||||
minutes = timeframe_minutes(tf)
|
||||
if minutes is None:
|
||||
return None
|
||||
paired = FAST_BSP_HTF_PAIR.get(str(tf).strip().lower())
|
||||
if paired:
|
||||
return paired, timeframe_minutes(paired)
|
||||
return f'{minutes * FAST_BSP_HTF_FALLBACK_RATIO}m', minutes * FAST_BSP_HTF_FALLBACK_RATIO
|
||||
|
||||
|
||||
class FastBspBuilderMixin:
|
||||
def build_fast_bsp_htf(self, df, timeframe=None):
|
||||
"""对同一份 df 重采样得到大级别,不额外拉数据。
|
||||
|
||||
大级别只用来取分型方向,样本太少就没有过滤意义,故重采样后不足 60 根时放弃。
|
||||
"""
|
||||
tf = timeframe or getattr(self, 'timeframe', None)
|
||||
htf = resolve_htf(tf)
|
||||
ltf_minutes = timeframe_minutes(tf)
|
||||
if htf is None or not ltf_minutes:
|
||||
return None
|
||||
label, minutes = htf
|
||||
if not minutes or len(df) * ltf_minutes < minutes * 60:
|
||||
return None
|
||||
try:
|
||||
from chanlun.pipeline.timeframe import TF_DF
|
||||
|
||||
return TF_DF(df, minutes, label)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def cal_fast_bsp(self, df=None, bi_zs_list=None, htf_chan=None, with_htf=True,
|
||||
timeframe=None, **kw):
|
||||
"""算第四类买卖点,返回 ChanFastBSP 列表。
|
||||
|
||||
bi_zs_list 传入已算好的 pure 笔中枢可免去重复计算。
|
||||
with_htf=False 时跳过大级别构建,只留 ladder_ok 这一个过滤标志。
|
||||
kw 透传给 find_fast_bsp3(scan / pullback_win / tol / require_touch 等)。
|
||||
"""
|
||||
src = df if df is not None else getattr(self, 'dataframe', None)
|
||||
if src is None or len(src) == 0:
|
||||
self.fast_bsp_list = []
|
||||
return self.fast_bsp_list
|
||||
|
||||
src = ensure_timestamp(src)
|
||||
if bi_zs_list is None:
|
||||
bi_zs_list = getattr(self, 'bi_zs_list', None)
|
||||
if not bi_zs_list:
|
||||
bi_zs_list = self.cal_bi_zs_list_pure(self.cal_bi_list(self.get_klc_list(self.cal_kl_data(src))))
|
||||
|
||||
zones = zones_from_zs_list(bi_zs_list, src)
|
||||
if zones.empty:
|
||||
self.fast_bsp_list = []
|
||||
return self.fast_bsp_list
|
||||
|
||||
zones = add_zone_ladder(zones)
|
||||
sig = find_fast_bsp3(src, zones, **kw)
|
||||
if sig.empty:
|
||||
self.fast_bsp_list = []
|
||||
return self.fast_bsp_list
|
||||
|
||||
sig = attach_zone_ladder(sig, zones)
|
||||
|
||||
if with_htf:
|
||||
if htf_chan is None:
|
||||
htf_chan = self.build_fast_bsp_htf(src, timeframe)
|
||||
sig = attach_htf_agree(sig, src, htf_fx_timeline(htf_chan) if htf_chan is not None else pd.DataFrame())
|
||||
else:
|
||||
sig['htf_dir'] = None
|
||||
sig['htf_agree'] = None
|
||||
|
||||
times = src['date'].dt.strftime('%Y-%m-%d %H:%M:%S').to_numpy()
|
||||
close = src['close'].to_numpy(dtype=float)
|
||||
|
||||
out = []
|
||||
for r in sig.itertuples(index=False):
|
||||
entry_idx = int(r.entry_idx)
|
||||
agree = getattr(r, 'htf_agree', None)
|
||||
htf_dir = getattr(r, 'htf_dir', None)
|
||||
out.append(ChanFastBSP(
|
||||
time=times[entry_idx],
|
||||
price=close[entry_idx],
|
||||
ddir=Chan_BSP_DIR.BUY if r.direction == 1 else Chan_BSP_DIR.SELL,
|
||||
entry_idx=entry_idx,
|
||||
bo_time=times[int(r.bo_idx)],
|
||||
pb_time=times[int(r.pb_idx)] if r.pb_idx == r.pb_idx else None,
|
||||
lag=r.lag,
|
||||
depth=r.depth,
|
||||
zg=r.zg,
|
||||
zd=r.zd,
|
||||
occ=r.occ,
|
||||
htf_dir=None if htf_dir is None or htf_dir != htf_dir else int(htf_dir),
|
||||
htf_agree=None if agree is None or agree != agree else bool(agree),
|
||||
ladder_ok=bool(r.ladder_ok),
|
||||
))
|
||||
self.fast_bsp_list = out
|
||||
return out
|
||||
@@ -1,171 +0,0 @@
|
||||
"""增量更新:新K只追加 KLU/KLC,笔与笔中枢在当前列表上重算。
|
||||
|
||||
不改 init_TF_DF 的整段语义。笔必须整表重扫:最后一笔 is_sure 允许收回
|
||||
(OWN_CHAN_ZS_001 上 60 天出现 7 次)。笔中枢用 cal_bi_zs_list_pure。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from chanlun.pipeline.resample import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_KLC_STATE
|
||||
from chanlun.core.ChanKLU import ChanKLU
|
||||
|
||||
|
||||
class IncrementalBuilderMixin:
|
||||
def init_stream(self, df, interval=1, timeframe=None):
|
||||
"""用历史K线初始化流式状态,之后用 append_bar / replace_last_bar。"""
|
||||
if df is None or df.empty:
|
||||
raise ValueError("DataFrame for stream is empty.")
|
||||
if "date" not in df.columns:
|
||||
raise ValueError(f"DataFrame missing 'date' column. Columns: {df.columns.tolist()}")
|
||||
self.timeframe = timeframe
|
||||
self.interval = interval
|
||||
if interval == 1:
|
||||
self.dataframe = df.copy()
|
||||
else:
|
||||
self.dataframe = resample_to_interval(df, interval)
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self.klu_list = []
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.bi_zs_list = []
|
||||
self.seg_list = []
|
||||
self.zs_list = []
|
||||
self.bsp_list = []
|
||||
self.klc_fx_list = []
|
||||
self.big_zs_list = []
|
||||
self._klc_feed_last_klu = None
|
||||
for i in range(len(self.dataframe)):
|
||||
self._append_row_at(i, rebuild=False)
|
||||
self.rebuild_bi_zs()
|
||||
return self
|
||||
|
||||
def append_bar(self, row):
|
||||
"""追加一根已收盘K线。同一时间戳则改为替换最后一根。"""
|
||||
self._ensure_stream_state()
|
||||
item = self._normalize_row(row)
|
||||
if self.klu_list and self.klu_list[-1].time == self._row_time_str(item):
|
||||
return self.replace_last_bar(item)
|
||||
self._append_item_to_dataframe(item)
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self._append_row_at(len(self.dataframe) - 1, rebuild=True)
|
||||
return self
|
||||
|
||||
def replace_last_bar(self, row):
|
||||
"""更新最后一根K(未完成K线走新OHLC)。包含关系从 KLU 列表重放。"""
|
||||
self._ensure_stream_state()
|
||||
if not self.klu_list:
|
||||
return self.append_bar(row)
|
||||
item = self._normalize_row(row)
|
||||
idx = self.dataframe.index[-1]
|
||||
for key, val in item.items():
|
||||
self.dataframe.at[idx, key] = val
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self._apply_item_to_klu(self.klu_list[-1], self.dataframe.iloc[-1])
|
||||
self._rebuild_klc_from_klu()
|
||||
self.rebuild_bi_zs()
|
||||
return self
|
||||
|
||||
def rebuild_bi_zs(self):
|
||||
"""在当前 KLC 上重算笔 + cal_bi_zs_list_pure。会先清分型标记。"""
|
||||
self._reset_klc_bi_marks(self.klc_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list) if self.klc_list else []
|
||||
self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list) if self.bi_list else []
|
||||
return self.bi_zs_list
|
||||
|
||||
def _ensure_stream_state(self):
|
||||
if not hasattr(self, "klu_list") or self.klu_list is None:
|
||||
self.klu_list = []
|
||||
if not hasattr(self, "klc_list") or self.klc_list is None:
|
||||
self.klc_list = []
|
||||
if not hasattr(self, "dataframe") or self.dataframe is None:
|
||||
self.dataframe = DataFrame(
|
||||
columns=["date", "open", "high", "low", "close", "volume"]
|
||||
)
|
||||
if not hasattr(self, "_klc_feed_last_klu"):
|
||||
self._klc_feed_last_klu = self.klu_list[-1] if self.klu_list else None
|
||||
if not hasattr(self, "bi_zs_list"):
|
||||
self.bi_zs_list = []
|
||||
|
||||
def _rebuild_klc_from_klu(self):
|
||||
self.klc_list = []
|
||||
last_klu = None
|
||||
for klu in self.klu_list:
|
||||
self._push_klu_into_klc_list(self.klc_list, klu, last_klu)
|
||||
last_klu = klu
|
||||
self._klc_feed_last_klu = last_klu
|
||||
|
||||
def _append_row_at(self, idx, rebuild=True):
|
||||
item = self.dataframe.iloc[idx]
|
||||
klu = self._klu_from_item(item, idx)
|
||||
if self.klu_list:
|
||||
self.klu_list[-1].set_next(klu)
|
||||
klu.set_pre(self.klu_list[-1])
|
||||
self._push_klu_into_klc_list(self.klc_list, klu, self._klc_feed_last_klu)
|
||||
self._klc_feed_last_klu = klu
|
||||
self.klu_list.append(klu)
|
||||
if rebuild:
|
||||
self.rebuild_bi_zs()
|
||||
|
||||
def _klu_from_item(self, item, idx):
|
||||
klu = ChanKLU(
|
||||
self._item_time_str(item),
|
||||
item["open"],
|
||||
item["high"],
|
||||
item["low"],
|
||||
item["close"],
|
||||
item["volume"],
|
||||
)
|
||||
klu.set_idx(idx)
|
||||
if not hasattr(klu, "ema13"):
|
||||
klu.ema13 = 0
|
||||
if "macd" in item:
|
||||
klu.set_indicators(item)
|
||||
return klu
|
||||
|
||||
def _apply_item_to_klu(self, klu, item):
|
||||
klu.time = self._item_time_str(item)
|
||||
klu.open = item["open"]
|
||||
klu.high = item["high"]
|
||||
klu.low = item["low"]
|
||||
klu.close = item["close"]
|
||||
klu.volume = item["volume"]
|
||||
klu.range = klu.high - klu.low
|
||||
klu.body = abs(klu.close - klu.open)
|
||||
if "macd" in item:
|
||||
klu.set_indicators(item)
|
||||
|
||||
def _reset_klc_bi_marks(self, klc_list):
|
||||
for klc in klc_list:
|
||||
klc.fx = Chan_FX_TYPE.UNKNOWN
|
||||
klc.klc_fx_type = Chan_KLC_FX.UNKNOWN
|
||||
klc.klc_state = Chan_KLC_STATE.UNKNOWN
|
||||
klc.bi = None
|
||||
klc.fx_confirmed = False
|
||||
|
||||
def _item_time_str(self, item):
|
||||
date = item["date"]
|
||||
if hasattr(date, "to_pydatetime"):
|
||||
date = date.to_pydatetime()
|
||||
if isinstance(date, datetime):
|
||||
return date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return str(date)
|
||||
|
||||
def _row_time_str(self, item):
|
||||
return self._item_time_str(item)
|
||||
|
||||
def _normalize_row(self, row):
|
||||
if isinstance(row, pd.Series):
|
||||
return row
|
||||
return pd.Series(row)
|
||||
|
||||
def _append_item_to_dataframe(self, item):
|
||||
row_df = DataFrame([item])
|
||||
if self.dataframe is None or self.dataframe.empty:
|
||||
self.dataframe = row_df
|
||||
else:
|
||||
self.dataframe = pd.concat([self.dataframe, row_df], ignore_index=True)
|
||||
@@ -6,8 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from chanlun.indicators import ta
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
@@ -54,8 +55,8 @@ class IndicatorsBuilderMixin:
|
||||
return None
|
||||
|
||||
def add_indicators(self, df):
|
||||
fast = 26
|
||||
slow = 52
|
||||
fast = 12
|
||||
slow = 26
|
||||
period = 9
|
||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
@@ -84,8 +86,7 @@ class KlineBuilderMixin:
|
||||
return Chan_FX_TYPE.UNKNOWN
|
||||
|
||||
def check_fx(self, klc):
|
||||
# 右K未完成(仍在包含合并)时不分型:否则确认笔会随 next 扩区间被 check_*_fx 收回
|
||||
if klc.pre and klc.next and klc.next.end_klu is not None:
|
||||
if klc.pre and klc.next:
|
||||
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low:
|
||||
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||
@@ -98,21 +99,6 @@ class KlineBuilderMixin:
|
||||
return Chan_FX_TYPE.BOTTOM
|
||||
return Chan_FX_TYPE.UNKNOWN
|
||||
|
||||
def check_fx3(self, klc):
|
||||
# 右K未完成(仍在包含合并)时不分型:否则确认笔会随 next 扩区间被 check_*_fx 收回
|
||||
if klc.pre and klc.next and klc.next.end_klu is not None:
|
||||
next_klu = klc.next.end_klu.next
|
||||
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low and klc.high > next_klu.high:
|
||||
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
|
||||
return Chan_FX_TYPE.TOP
|
||||
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high and klc.low < next_klu.low:
|
||||
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
|
||||
klc.set_fx(Chan_FX_TYPE.BOTTOM)
|
||||
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
|
||||
return Chan_FX_TYPE.BOTTOM
|
||||
return Chan_FX_TYPE.UNKNOWN
|
||||
def check_fx2(self, klc):
|
||||
if klc.pre and klc.next:
|
||||
if klc.high > klc.pre.close and klc.close > klc.next.close and klc.close > klc.pre.close and klc.close > klc.next.close:
|
||||
@@ -147,109 +133,109 @@ class KlineBuilderMixin:
|
||||
return df['volume_ratio']
|
||||
|
||||
def cal_kl_data(self, dataframe:DataFrame):
|
||||
"""按行构造 KLU 链。
|
||||
|
||||
这里刻意不用 `dataframe.iloc[i]`:那会为每一根新建一个几十列的 Series,
|
||||
随后 set_indicators 再在其上做几十次逐键查找。实测这两件事合计占 TF_DF
|
||||
构建耗时的 96%。改为先把用到的列取成 ndarray,循环里只做整数下标访问。
|
||||
"""
|
||||
n = len(dataframe)
|
||||
if n == 0:
|
||||
return []
|
||||
|
||||
times = self._format_times(dataframe['date'])
|
||||
o_a = dataframe['open'].to_numpy(dtype=float)
|
||||
h_a = dataframe['high'].to_numpy(dtype=float)
|
||||
l_a = dataframe['low'].to_numpy(dtype=float)
|
||||
c_a = dataframe['close'].to_numpy(dtype=float)
|
||||
v_a = dataframe['volume'].to_numpy(dtype=float)
|
||||
|
||||
has_ind = 'macd' in dataframe.columns
|
||||
ind_cols = {}
|
||||
if has_ind:
|
||||
for _attr, col in ChanKLU.INDICATOR_FIELDS:
|
||||
if col in dataframe.columns:
|
||||
ind_cols[col] = dataframe[col].to_numpy(dtype=float)
|
||||
|
||||
fields = "time,open,high,low,close,volume"
|
||||
klu_list = []
|
||||
last_klu = None
|
||||
for i in range(n):
|
||||
klu = ChanKLU(times[i], o_a[i], h_a[i], l_a[i], c_a[i], v_a[i])
|
||||
for i in range(0, len(dataframe)):
|
||||
item = dataframe.iloc[i]
|
||||
date = item['date']
|
||||
o = item['open']
|
||||
h = item['high']
|
||||
l = item['low']
|
||||
c = item['close']
|
||||
v = item['volume']
|
||||
# time_obj = date.fromtimestamp(date)
|
||||
# date = date + timedelta(hours=8)
|
||||
time_str = date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
item_data = [
|
||||
time_str,
|
||||
o,
|
||||
h,
|
||||
l,
|
||||
c,
|
||||
v
|
||||
]
|
||||
# klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)))
|
||||
klu = ChanKLU(time_str, o, h, l, c, v)
|
||||
# print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume)
|
||||
klu.set_idx(i)
|
||||
klu_list.append(klu)
|
||||
if last_klu:
|
||||
last_klu.set_next(klu)
|
||||
klu.set_pre(last_klu)
|
||||
last_klu = klu
|
||||
if has_ind:
|
||||
klu.set_indicators_from(ind_cols, i)
|
||||
if 'macd' in item:
|
||||
klu.set_indicators(item)
|
||||
return klu_list
|
||||
|
||||
@staticmethod
|
||||
def _format_times(col):
|
||||
"""向量化 strftime。非 datetime 列(少见)退回逐个格式化。"""
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
try:
|
||||
return col.dt.strftime(fmt).to_numpy()
|
||||
except AttributeError:
|
||||
return np.array([d.strftime(fmt) for d in col], dtype=object)
|
||||
|
||||
def get_kl_data(self, dataframe:DataFrame):
|
||||
return self.cal_kl_data(dataframe)
|
||||
|
||||
def _push_klu_into_klc_list(self, klc_list, klu, last_klu):
|
||||
"""把一根 KLU 并入包含K线列表。与 get_klc_list 的几何规则相同。"""
|
||||
if len(klc_list) > 0:
|
||||
last_klc = klc_list[-1]
|
||||
if klu.exception:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc.high = klu.close if klu.close > klu.open else klu.open
|
||||
klc.low = klu.open if klu.close > klu.open else klu.close
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
else:
|
||||
included = last_klc.check_klu_included(klu)
|
||||
if not included:
|
||||
def get_klc_list(self, klu_list):
|
||||
klc_list = []
|
||||
last_klu = None
|
||||
# ChanMACD.__init__ 已调用 cal_macd_state,切勿再调一次(会重复堆积 seg/unittf)
|
||||
macd = ChanMACD(klu_list)
|
||||
klu_list = macd.klu_list
|
||||
self._last_chan_macd = macd
|
||||
ema_up_list = []
|
||||
ema_down_list = []
|
||||
ema_up_count = 0
|
||||
ema_down_count = 0
|
||||
last_klu = None
|
||||
for klu in klu_list:
|
||||
ema = klu.ema52
|
||||
last_ema = last_klu.ema52 if last_klu else 0
|
||||
if klu.close >= ema:
|
||||
ema_up_count += 1
|
||||
elif klu.close < ema:
|
||||
ema_down_count += 1
|
||||
if last_klu and last_klu.close >= last_ema and klu.close < ema:
|
||||
ema_up_list.append(ema_up_count)
|
||||
#print(last_klu.time, ema_up_count, "UP END")
|
||||
ema_up_count = 0
|
||||
elif last_klu and last_klu.close < last_ema and klu.close >= ema:
|
||||
ema_down_list.append(ema_down_count)
|
||||
#print(last_klu.time, ema_down_count, "DOWN END")
|
||||
ema_down_count = 0
|
||||
if len(klc_list) > 0:
|
||||
last_klc = klc_list[-1]
|
||||
if klu.exception:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc.high = klu.close if klu.close > klu.open else klu.open
|
||||
klc.low = klu.open if klu.close > klu.open else klu.close
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
#print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception)
|
||||
else:
|
||||
last_klc.add_klu(klu)
|
||||
else:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
if klu.open > klu.close:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
klc = ChanKLC(klu, 0, ddir)
|
||||
klc_list.append(klc)
|
||||
|
||||
def get_klc_list(self, klu_list):
|
||||
klc_list = []
|
||||
# ChanMACD.__init__ 已调用 cal_macd_state,切勿再调一次(会重复堆积 seg/unittf)。
|
||||
# lean 模式跳过整套 MACD 状态机:它只服务于 bsp/背驰/web 展示,笔与中枢不依赖它。
|
||||
if getattr(self, 'lean', False):
|
||||
self._last_chan_macd = None
|
||||
else:
|
||||
macd = ChanMACD(klu_list)
|
||||
klu_list = macd.klu_list
|
||||
self._last_chan_macd = macd
|
||||
|
||||
last_klu = None
|
||||
for klu in klu_list:
|
||||
self._push_klu_into_klc_list(klc_list, klu, last_klu)
|
||||
included = last_klc.check_klu_included(klu)
|
||||
if not included:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
else:
|
||||
last_klc.add_klu(klu)
|
||||
else:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
if klu.open > klu.close:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
klc = ChanKLC(klu, 0, ddir)
|
||||
klc_list.append(klc)
|
||||
last_klu = klu
|
||||
klc_list = self.cal_trend(klc_list)
|
||||
#print(ema52_up_list, ema52_down_list)
|
||||
return klc_list
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
@@ -353,12 +355,9 @@ class ZsBuilderMixin:
|
||||
return bi_zs_list
|
||||
|
||||
def get_zs_range(bis):
|
||||
bis_list = bis[0:3]
|
||||
zg = min(bi.high for bi in bis_list)
|
||||
zd = max(bi.low for bi in bis_list)
|
||||
dd = min(bi.low for bi in bis_list)
|
||||
gg = max(bi.high for bi in bis_list)
|
||||
return zg, zd, dd, gg
|
||||
zg = min(bi.high for bi in bis)
|
||||
zd = max(bi.low for bi in bis)
|
||||
return zg, zd
|
||||
|
||||
def is_bi_overlap_range(bi, zg, zd):
|
||||
return bi.high >= zd and bi.low <= zg
|
||||
@@ -376,8 +375,8 @@ class ZsBuilderMixin:
|
||||
zs.bi_list = list(bis)
|
||||
for bi in zs.bi_list:
|
||||
bi.set_bi_zs(zs)
|
||||
#zs.set_gg(max(bi.high for bi in zs.bi_list))
|
||||
#zs.set_dd(min(bi.low for bi in zs.bi_list))
|
||||
zs.set_gg(max(bi.high for bi in zs.bi_list))
|
||||
zs.set_dd(min(bi.low for bi in zs.bi_list))
|
||||
zs.classify_zs()
|
||||
|
||||
last_zs = None
|
||||
@@ -395,7 +394,7 @@ class ZsBuilderMixin:
|
||||
start_idx += 1
|
||||
continue
|
||||
|
||||
zg, zd, dd, gg = get_zs_range([bi1, bi2, bi3])
|
||||
zg, zd = get_zs_range([bi1, bi2, bi3])
|
||||
if zg <= zd:
|
||||
start_idx += 1
|
||||
continue
|
||||
@@ -421,8 +420,6 @@ class ZsBuilderMixin:
|
||||
zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir)
|
||||
zs.set_zg(zg)
|
||||
zs.set_zd(zd)
|
||||
zs.set_dd(dd)
|
||||
zs.set_gg(gg)
|
||||
|
||||
set_zs_bi_list(zs, bis_for_zs)
|
||||
zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time)
|
||||
|
||||
@@ -17,7 +17,9 @@ from chanlun.core.ChanSBI import ChanSBI
|
||||
from chanlun.core.ChanSEG import ChanSEG
|
||||
from chanlun.core.ChanZS import ChanZS
|
||||
from chanlun.core.ChanBSP import ChanBSP
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
from technical.util import resample_to_interval
|
||||
from decimal import Decimal
|
||||
import numpy as np
|
||||
from chanlun.indicators.ChanMACD import ChanMACD
|
||||
@@ -169,8 +171,6 @@ class ChanLun():
|
||||
return self.tf_df.find_second_bsp(bi_list, first_bsp_list)
|
||||
def find_all_bsp(self, bi_list, bi_zs_list):
|
||||
return self.tf_df.find_all_bsp(bi_list, bi_zs_list)
|
||||
def cal_fast_bsp(self, df=None, bi_zs_list=None, htf_chan=None, with_htf=True, timeframe=None, **kw):
|
||||
return self.tf_df.cal_fast_bsp(df, bi_zs_list, htf_chan, with_htf, timeframe, **kw)
|
||||
def get_zs_list(self, bi_list, seg_list):
|
||||
return self.tf_df.get_zs_list(bi_list, seg_list)
|
||||
def cal_bi_zs(self, seg_list):
|
||||
@@ -178,15 +178,6 @@ class ChanLun():
|
||||
def cal_bi_zs_list(self, bi_list):
|
||||
#return self.tf_df.cal_bi_zs(bi_list)
|
||||
return self.tf_df.cal_bi_zs_list(bi_list)
|
||||
def cal_bi_zs_list_pure(self, bi_list):
|
||||
return self.tf_df.cal_bi_zs_list_pure(bi_list)
|
||||
def init_stream(self, dataframe, interval=1, timeframe=None):
|
||||
self.tf_df.init_stream(dataframe, interval, timeframe)
|
||||
return self.tf_df
|
||||
def append_bar(self, row):
|
||||
return self.tf_df.append_bar(row)
|
||||
def replace_last_bar(self, row):
|
||||
return self.tf_df.replace_last_bar(row)
|
||||
def get_bi_zs_list(self, bi_list):
|
||||
return self.tf_df.get_bi_zs_list(bi_list)
|
||||
def get_decimal(self, value):
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
"""OHLCV resampling — replaces `technical.util.resample_to_interval`.
|
||||
|
||||
That was the only symbol this project imported from `technical`, which in turn
|
||||
pulled in the freqtrade dependency chain. Behaviour is preserved exactly,
|
||||
including the left-labelled bins (rows are candle *open* times) and the
|
||||
`dropna()` that drops empty intervals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
__all__ = ["TICKER_INTERVAL_MINUTES", "resample_to_interval"]
|
||||
|
||||
TICKER_INTERVAL_MINUTES: dict[str, int] = {
|
||||
"1m": 1,
|
||||
"5m": 5,
|
||||
"15m": 15,
|
||||
"30m": 30,
|
||||
"1h": 60,
|
||||
"60m": 60,
|
||||
"2h": 120,
|
||||
"4h": 240,
|
||||
"6h": 360,
|
||||
"12h": 720,
|
||||
"1d": 1440,
|
||||
"1w": 10080,
|
||||
}
|
||||
|
||||
_OHLC_AGG = {
|
||||
"open": "first",
|
||||
"high": "max",
|
||||
"low": "min",
|
||||
"close": "last",
|
||||
"volume": "sum",
|
||||
}
|
||||
|
||||
|
||||
def resample_to_interval(dataframe: pd.DataFrame, interval: int | str) -> pd.DataFrame:
|
||||
"""Resample OHLCV rows to `interval` minutes (or a timeframe string).
|
||||
|
||||
Merging the result back onto a finer frame requires care to avoid lookahead
|
||||
bias; this function only resamples.
|
||||
"""
|
||||
if isinstance(interval, str):
|
||||
interval = TICKER_INTERVAL_MINUTES[interval]
|
||||
|
||||
df = dataframe.copy()
|
||||
df = df.set_index(pd.DatetimeIndex(df["date"]))
|
||||
df = df.resample(f"{interval}min", label="left").agg(_OHLC_AGG).dropna()
|
||||
df.reset_index(inplace=True)
|
||||
return df
|
||||
@@ -2,8 +2,9 @@ from datetime import timedelta
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from chanlun.pipeline.resample import resample_to_interval
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanBI import ChanBI
|
||||
from chanlun.core.ChanBIZS import ChanBIZS
|
||||
@@ -30,26 +31,16 @@ from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||
from chanlun.indicators.ChanMACD import ChanMACD
|
||||
from chanlun.pipeline.builders.bi import BiBuilderMixin
|
||||
from chanlun.pipeline.builders.bsp import BspBuilderMixin
|
||||
from chanlun.pipeline.builders.fast_bsp import FastBspBuilderMixin
|
||||
from chanlun.pipeline.builders.incremental import IncrementalBuilderMixin
|
||||
from chanlun.pipeline.builders.indicators import IndicatorsBuilderMixin
|
||||
from chanlun.pipeline.builders.kline import KlineBuilderMixin
|
||||
from chanlun.pipeline.builders.seg import SegBuilderMixin
|
||||
from chanlun.pipeline.builders.zs import ZsBuilderMixin
|
||||
|
||||
class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin, FastBspBuilderMixin, IncrementalBuilderMixin):
|
||||
def __init__(self, df=None, interval=0, timeframe=None, lean=False):
|
||||
"""lean=True 只构建到中枢,跳过线段/走势中枢/MACD 状态机。
|
||||
|
||||
研究与实盘只吃 bi_list → 中枢 → fast_bsp 这条链;线段、zs、big_zs 和整套
|
||||
MACD 背驰状态机是 web 展示与 bsp_list 才用的。实测这些占全量构建的约四成。
|
||||
注意 lean 下 bsp_list/seg_list/chanmacd 均为空,**不要给 web 用**。
|
||||
"""
|
||||
self.lean = lean
|
||||
class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin):
|
||||
def __init__(self, df=None, interval=0, timeframe=None):
|
||||
if df is not None:
|
||||
self.init_TF_DF(df, interval, timeframe, lean=lean)
|
||||
def init_TF_DF(self, df, interval, timeframe, lean=False):
|
||||
self.lean = lean
|
||||
self.init_TF_DF(df, interval, timeframe)
|
||||
def init_TF_DF(self, df, interval, timeframe):
|
||||
self.timeframe = timeframe
|
||||
self.interval = interval
|
||||
# 检查 DataFrame 是否为空或没有 date 列
|
||||
@@ -68,19 +59,12 @@ class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilde
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.zs_list = []
|
||||
self.bi_zs_list = []
|
||||
self.bsp_list = []
|
||||
self.fast_bsp_list = []
|
||||
self.seg_list = []
|
||||
self.klc_fx_list = []
|
||||
self.klu_list = self.cal_kl_data(self.dataframe)
|
||||
self.klc_list = self.get_klc_list(self.klu_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||
self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list)
|
||||
if self.lean:
|
||||
self.big_zs_list = []
|
||||
self.chanmacd = None
|
||||
return
|
||||
self.seg_list = self.get_seg_list(self.bi_list)
|
||||
self.zs_list = self.get_zs_list(self.bi_list, self.seg_list)
|
||||
self.big_zs_list = self.get_big_zs_list(self.zs_list)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import annotations
|
||||
@@ -1,141 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
_CHAN = Path(__file__).resolve().parents[2]
|
||||
if str(_CHAN) not in sys.path:
|
||||
sys.path.insert(0, str(_CHAN))
|
||||
|
||||
from chanlun.pipeline.timeframe import TF_DF # noqa: E402
|
||||
|
||||
|
||||
def _zigzag_df(n=160, step=8):
|
||||
dates = pd.date_range("2024-01-01", periods=n, freq="5min")
|
||||
rows = []
|
||||
price = 100.0
|
||||
for i, date in enumerate(dates):
|
||||
up = (i // step) % 2 == 0
|
||||
if up:
|
||||
o = price
|
||||
c = price + 1.5
|
||||
h = c + 0.3
|
||||
l = o - 0.2
|
||||
else:
|
||||
o = price
|
||||
c = price - 1.5
|
||||
h = o + 0.2
|
||||
l = c - 0.3
|
||||
price = c
|
||||
rows.append(
|
||||
{
|
||||
"date": date,
|
||||
"open": o,
|
||||
"high": h,
|
||||
"low": l,
|
||||
"close": c,
|
||||
"volume": 1.0,
|
||||
}
|
||||
)
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def _sure_bi_key(bi):
|
||||
return (str(bi.start_time), bi.dir.name, round(float(bi.high), 6), round(float(bi.low), 6))
|
||||
|
||||
|
||||
def _zs_key(zs):
|
||||
return (
|
||||
str(zs.start_time),
|
||||
round(float(zs.zg), 6),
|
||||
round(float(zs.zd), 6),
|
||||
len(zs.bi_list),
|
||||
)
|
||||
|
||||
|
||||
class TestIncremental(unittest.TestCase):
|
||||
def test_init_stream_matches_batch_push(self):
|
||||
df = _zigzag_df()
|
||||
stream = TF_DF()
|
||||
stream.init_stream(df, 1, "5m")
|
||||
|
||||
batch = TF_DF()
|
||||
indexed = batch.add_indicators(df.copy())
|
||||
klu = batch.cal_kl_data(indexed)
|
||||
klc = []
|
||||
last = None
|
||||
for k in klu:
|
||||
batch._push_klu_into_klc_list(klc, k, last)
|
||||
last = k
|
||||
batch.klc_list = klc
|
||||
batch.rebuild_bi_zs()
|
||||
|
||||
self.assertEqual(len(stream.klu_list), len(klu))
|
||||
self.assertEqual(len(stream.klc_list), len(klc))
|
||||
self.assertEqual(
|
||||
[_sure_bi_key(b) for b in stream.bi_list if b.is_sure],
|
||||
[_sure_bi_key(b) for b in batch.bi_list if b.is_sure],
|
||||
)
|
||||
self.assertEqual(
|
||||
[_zs_key(z) for z in stream.bi_zs_list],
|
||||
[_zs_key(z) for z in batch.bi_zs_list],
|
||||
)
|
||||
|
||||
def test_append_bar_matches_init_stream(self):
|
||||
df = _zigzag_df()
|
||||
stream = TF_DF()
|
||||
stream.init_stream(df, 1, "5m")
|
||||
|
||||
inc = TF_DF()
|
||||
for _, row in df.iterrows():
|
||||
inc.append_bar(row)
|
||||
|
||||
self.assertEqual(len(inc.klu_list), len(stream.klu_list))
|
||||
self.assertEqual(len(inc.klc_list), len(stream.klc_list))
|
||||
self.assertEqual(
|
||||
[_sure_bi_key(b) for b in inc.bi_list if b.is_sure],
|
||||
[_sure_bi_key(b) for b in stream.bi_list if b.is_sure],
|
||||
)
|
||||
self.assertEqual(
|
||||
[_zs_key(z) for z in inc.bi_zs_list],
|
||||
[_zs_key(z) for z in stream.bi_zs_list],
|
||||
)
|
||||
|
||||
def test_replace_last_bar_keeps_count(self):
|
||||
df = _zigzag_df(n=80)
|
||||
tf = TF_DF()
|
||||
tf.init_stream(df, 1, "5m")
|
||||
n_klu = len(tf.klu_list)
|
||||
last = df.iloc[-1].copy()
|
||||
last["close"] = float(last["close"]) + 0.01
|
||||
last["high"] = max(float(last["high"]), float(last["close"]))
|
||||
tf.replace_last_bar(last)
|
||||
self.assertEqual(len(tf.klu_list), n_klu)
|
||||
self.assertGreater(len(tf.klc_list), 0)
|
||||
|
||||
def test_check_fx_skips_forming_right_wing(self):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from chanlun.core.ChanEnum import Chan_FX_TYPE
|
||||
|
||||
tf = TF_DF()
|
||||
pre = SimpleNamespace(high=10, low=8)
|
||||
nxt_open = SimpleNamespace(high=11, low=7, end_klu=None)
|
||||
nxt_done = SimpleNamespace(high=11, low=7, end_klu=object())
|
||||
center = SimpleNamespace(
|
||||
pre=pre,
|
||||
next=nxt_open,
|
||||
high=12,
|
||||
low=9,
|
||||
set_fx=lambda *_a, **_k: None,
|
||||
)
|
||||
self.assertEqual(tf.check_fx(center), Chan_FX_TYPE.UNKNOWN)
|
||||
center.next = nxt_done
|
||||
self.assertEqual(tf.check_fx(center), Chan_FX_TYPE.TOP)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,198 +0,0 @@
|
||||
"""Pin chanlun.indicators.ta to TA-Lib's output, bar for bar.
|
||||
|
||||
These indicators feed the Chan structure builders, so a one-bar shift in the
|
||||
warm-up or a different smoothing seed silently changes every downstream
|
||||
bi/seg/zs. Equality against the reference implementation is the only check
|
||||
that catches that.
|
||||
|
||||
Skipped when talib is unavailable — which is the point of the replacement, so
|
||||
the suite still has to pass without it. Run in an environment that has talib
|
||||
whenever chanlun/indicators/ta.py changes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from chanlun.indicators import ta # noqa: E402
|
||||
from chanlun.pipeline.resample import resample_to_interval # noqa: E402
|
||||
|
||||
try:
|
||||
import talib.abstract as reference
|
||||
except ImportError: # pragma: no cover
|
||||
reference = None
|
||||
|
||||
requires_talib = unittest.skipIf(reference is None, "talib not installed")
|
||||
|
||||
|
||||
def make_ohlcv(n: int = 900, seed: int = 7) -> pd.DataFrame:
|
||||
"""Random walk with enough range for BBANDS(365) and EMA(208) to warm up."""
|
||||
rng = np.random.default_rng(seed)
|
||||
close = 100.0 + np.cumsum(rng.normal(0.0, 1.0, n))
|
||||
spread = np.abs(rng.normal(0.0, 0.6, n)) + 0.05
|
||||
high = close + spread
|
||||
low = close - spread
|
||||
open_ = np.concatenate([[close[0]], close[:-1]])
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"date": pd.date_range("2024-01-01", periods=n, freq="1min", tz="UTC"),
|
||||
"open": open_,
|
||||
"high": np.maximum.reduce([high, open_, close]),
|
||||
"low": np.minimum.reduce([low, open_, close]),
|
||||
"close": close,
|
||||
"volume": rng.uniform(1.0, 100.0, n),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TAEquivalence(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.df = make_ohlcv()
|
||||
|
||||
def assertSameSeries(self, got, expected, label: str) -> None:
|
||||
g = np.asarray(got, dtype=float)
|
||||
e = np.asarray(expected, dtype=float)
|
||||
self.assertEqual(g.shape, e.shape, f"{label}: shape")
|
||||
np.testing.assert_array_equal(
|
||||
np.isnan(g), np.isnan(e), err_msg=f"{label}: NaN warm-up differs"
|
||||
)
|
||||
mask = ~np.isnan(e)
|
||||
np.testing.assert_allclose(
|
||||
g[mask], e[mask], rtol=1e-9, atol=1e-8, err_msg=f"{label}: values differ"
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_sma(self) -> None:
|
||||
for period in (5, 20, 90, 250):
|
||||
self.assertSameSeries(
|
||||
ta.SMA(self.df, timeperiod=period),
|
||||
reference.SMA(self.df, timeperiod=period),
|
||||
f"SMA({period})",
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_ma(self) -> None:
|
||||
for period in (5, 10, 250):
|
||||
self.assertSameSeries(
|
||||
ta.MA(self.df, timeperiod=period),
|
||||
reference.MA(self.df, timeperiod=period),
|
||||
f"MA({period})",
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_ema(self) -> None:
|
||||
for period in (5, 7, 10, 13, 24, 26, 30, 52, 104, 156, 208):
|
||||
self.assertSameSeries(
|
||||
ta.EMA(self.df, timeperiod=period),
|
||||
reference.EMA(self.df, timeperiod=period),
|
||||
f"EMA({period})",
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_rsi(self) -> None:
|
||||
for period in (7, 14, 21):
|
||||
self.assertSameSeries(
|
||||
ta.RSI(self.df, timeperiod=period),
|
||||
reference.RSI(self.df, timeperiod=period),
|
||||
f"RSI({period})",
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_atr(self) -> None:
|
||||
for period in (7, 14, 30):
|
||||
self.assertSameSeries(
|
||||
ta.ATR(self.df, timeperiod=period),
|
||||
reference.ATR(self.df, timeperiod=period),
|
||||
f"ATR({period})",
|
||||
)
|
||||
|
||||
@requires_talib
|
||||
def test_macd(self) -> None:
|
||||
for fast, slow, signal in ((12, 26, 9), (26, 52, 9), (5, 35, 5)):
|
||||
got = ta.MACD(self.df, fastperiod=fast, slowperiod=slow, signalperiod=signal)
|
||||
exp = reference.MACD(self.df, fastperiod=fast, slowperiod=slow, signalperiod=signal)
|
||||
for col in ("macd", "macdsignal", "macdhist"):
|
||||
self.assertSameSeries(got[col], exp[col], f"MACD({fast},{slow},{signal}).{col}")
|
||||
|
||||
@requires_talib
|
||||
def test_bbands(self) -> None:
|
||||
cases = (
|
||||
(365, 3.0, 3.0),
|
||||
(120, 3.0, 3.0),
|
||||
(41, 2.3, 2.3),
|
||||
(41, 2.0, 2.0),
|
||||
(26, 3.0, 3.0),
|
||||
(20, 2.0, 2.0),
|
||||
(14, 2.0, 2.0),
|
||||
)
|
||||
for period, up, dn in cases:
|
||||
got = ta.BBANDS(self.df, timeperiod=period, nbdevup=up, nbdevdn=dn, matype=0)
|
||||
exp = reference.BBANDS(self.df, timeperiod=period, nbdevup=up, nbdevdn=dn, matype=0)
|
||||
for col in ("upperband", "middleband", "lowerband"):
|
||||
self.assertSameSeries(got[col], exp[col], f"BBANDS({period},{up},{dn}).{col}")
|
||||
|
||||
@requires_talib
|
||||
def test_bbands_is_more_accurate_than_talib_on_tiny_windows(self) -> None:
|
||||
"""A deliberate divergence, documented so nobody "fixes" it back.
|
||||
|
||||
TA-Lib derives the variance from sumsq/n - mean**2, which cancels
|
||||
catastrophically when the window is short and prices are far from zero;
|
||||
at timeperiod=2 it drifts ~1e-6. Rolling std is accurate there, so the
|
||||
two disagree. No timeperiod below 14 is used in this codebase, and the
|
||||
periods that are used agree to ~1e-10 (covered by test_bbands).
|
||||
"""
|
||||
got = ta.BBANDS(self.df, timeperiod=2, nbdevup=1.0, nbdevdn=1.0, matype=0)["upperband"]
|
||||
exp = reference.BBANDS(self.df, timeperiod=2, nbdevup=1.0, nbdevdn=1.0, matype=0)["upperband"]
|
||||
|
||||
window = self.df["close"].rolling(2)
|
||||
truth = window.mean() + window.std(ddof=0)
|
||||
ours = np.nanmax(np.abs((got - truth).to_numpy()))
|
||||
theirs = np.nanmax(np.abs((exp - truth).to_numpy()))
|
||||
self.assertLess(ours, 1e-9)
|
||||
self.assertLess(ours, theirs)
|
||||
|
||||
@requires_talib
|
||||
def test_matches_on_real_price_scale(self) -> None:
|
||||
"""Guard against tolerances that only hold near 100."""
|
||||
df = self.df.copy()
|
||||
for col in ("open", "high", "low", "close"):
|
||||
df[col] *= 900.0
|
||||
self.assertSameSeries(
|
||||
ta.ATR(df, timeperiod=14), reference.ATR(df, timeperiod=14), "ATR@scale"
|
||||
)
|
||||
self.assertSameSeries(
|
||||
ta.RSI(df, timeperiod=14), reference.RSI(df, timeperiod=14), "RSI@scale"
|
||||
)
|
||||
|
||||
|
||||
class ResampleEquivalence(unittest.TestCase):
|
||||
@unittest.skipIf(
|
||||
__import__("importlib").util.find_spec("technical") is None,
|
||||
"technical not installed",
|
||||
)
|
||||
def test_matches_technical(self) -> None:
|
||||
from technical.util import resample_to_interval as ref_resample
|
||||
|
||||
df = make_ohlcv(600)
|
||||
for interval in (5, 15, 60, "5m", "1h"):
|
||||
got = resample_to_interval(df, interval)
|
||||
exp = ref_resample(df, interval)
|
||||
pd.testing.assert_frame_equal(got, exp, check_exact=False, rtol=1e-12)
|
||||
|
||||
def test_shapes_without_reference(self) -> None:
|
||||
df = make_ohlcv(120)
|
||||
out = resample_to_interval(df, 5)
|
||||
self.assertEqual(list(out.columns), ["date", "open", "high", "low", "close", "volume"])
|
||||
self.assertLessEqual(len(out), 120 // 5 + 1)
|
||||
self.assertTrue((out["high"] >= out["low"]).all())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.btc_chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8882,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.btc_perpetual.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short": true,
|
||||
"timeframe": "1m",
|
||||
"process_only_new_candles": false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "YOUR_BINANCE_API_KEY",
|
||||
"secret": "YOUR_BINANCE_API_SECRET",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
"chat_id": "YOUR_TELEGRAM_CHAT_ID"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8820,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "change_me_to_a_random_secret_key",
|
||||
"ws_token": "change_me_to_a_random_ws_token",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "BTC_Perpetual_Bot",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8800,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8811,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_1m.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": false
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {
|
||||
"proxies": {
|
||||
"http": "http://127.0.0.1:7897",
|
||||
"https": "http://127.0.0.1:7897"
|
||||
}
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"aiohttp_proxy": "http://127.0.0.1:7897"
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "8197349375:AAH208JghCq8raFYF-IpnobYknCr6iGDH_0",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8814,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8813,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_5m.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "5m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 5,
|
||||
"exit": 5,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": false
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {
|
||||
"proxies": {
|
||||
"http": "http://127.0.0.1:7897",
|
||||
"https": "http://127.0.0.1:7897"
|
||||
}
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"aiohttp_proxy": "http://127.0.0.1:7897"
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_60.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8814,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_k.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8815,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_k.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short": true,
|
||||
"timeframe": "1m",
|
||||
"process_only_new_candles": false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8815,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8820,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8820,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_eth_60.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"ETH/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8813,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "5m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "other",
|
||||
"use_order_book": false,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "other",
|
||||
"use_order_book": false,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8813,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_sol.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8811,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 2,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.95,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_sol_optimized.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short": true,
|
||||
"timeframe": "5m",
|
||||
"process_only_new_candles": false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 2,
|
||||
"exit": 2,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "other",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"ccxt_config": {
|
||||
"options": {"defaultType": "swap"}
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 1000,
|
||||
"timeout": 30000
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": []
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "",
|
||||
"chat_id": ""
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"jwt_secret_key": "",
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"webhook": "",
|
||||
"webhook_avatar": "",
|
||||
"poll_delay_seconds": 10
|
||||
},
|
||||
"notification_settings": {
|
||||
"status": "on",
|
||||
"status_inactive_after": 7,
|
||||
"timeframe_condition_change": "on",
|
||||
"telegram": { },
|
||||
"discord": { },
|
||||
"notify_all": true
|
||||
},
|
||||
"bot_name": "SOL_Chan_Optimized",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.007,
|
||||
"stoploss_range_step": 0.001,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "market",
|
||||
"emergency_exit": "market",
|
||||
"force_exit": "market",
|
||||
"force_entry": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
},
|
||||
"order_time_in_force": {
|
||||
"entry": "GTC",
|
||||
"exit": "GTC"
|
||||
},
|
||||
"strategy_path": "./user_data/Chan/strategies/",
|
||||
"strategy": "ChanLun_SOL_Optimized",
|
||||
"minimal_roi": {
|
||||
"0": 0.012,
|
||||
"120": 0.010,
|
||||
"240": 0.007,
|
||||
"360": 0.005
|
||||
},
|
||||
"stoploss": -0.007,
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.003,
|
||||
"trailing_stop_positive_offset": 0.005,
|
||||
"trailing_only_offset_is_reached": true,
|
||||
"use_custom_stoploss": true,
|
||||
"max_open_trades_per_pair": 1,
|
||||
"dry_run_wallet_refresh_time": 5,
|
||||
"caches": {
|
||||
"dataframe": {
|
||||
"enabled": true,
|
||||
"refresh_period": 60
|
||||
},
|
||||
"strategy": {
|
||||
"enabled": true,
|
||||
"refresh_period": 300
|
||||
}
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"config": {
|
||||
"pairs": ["SOL/USDT:USDT"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_sol.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : true,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"freqai": {
|
||||
"enabled": true,
|
||||
"purge_old_models": true,
|
||||
"train_period_days": 30,
|
||||
"backtest_period_days": 7,
|
||||
"identifier": "chanLun1",
|
||||
"live_retrain_hours": 1,
|
||||
"expiration_hours": 48,
|
||||
"fit_live_predictions_candles": 0,
|
||||
"data_kitchen_thread_count": 4,
|
||||
"save_backtest_models": true,
|
||||
"save_metadata": true,
|
||||
"feature_parameters": {
|
||||
"include_timeframes": [
|
||||
"1m",
|
||||
"5m",
|
||||
"15m"
|
||||
],
|
||||
"include_corr_pairlist": [
|
||||
"BTC/USDT:USDT",
|
||||
"ETH/USDT:USDT"
|
||||
],
|
||||
"label_period_candles": 24,
|
||||
"include_shifted_candles": 2,
|
||||
"indicator_periods_candles": [10, 20, 30],
|
||||
"allow_duplicate_train": true
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.25
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 100,
|
||||
"learning_rate": 0.1,
|
||||
"max_depth": 5,
|
||||
"subsample": 0.8,
|
||||
"colsample_bytree": 0.8,
|
||||
"use_label_for_weight": true,
|
||||
"booster": "gbtree",
|
||||
"num_class": 2
|
||||
}
|
||||
},
|
||||
"freqaimodel": "XGBoostClassifier",
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8811,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.btc_chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "30m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "ocQUqAPSD9PDhIL2lTMlMan0wMFwvvu5Fv8eYF3wUM8yPytm2jBgz51cgiHXw7J6",
|
||||
"secret": "yHIc6FOnSoOI2FvygpRKKku4FKaZGI5DSwC83Ip4wRfUcxszennF6hy2vhbVuLYJ",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT",
|
||||
"ETH/USDT:USDT",
|
||||
"SOL/USDT:USDT",
|
||||
"WIF/USDT:USDT",
|
||||
"1000PEPE/USDT:USDT",
|
||||
"DOGS/USDT:USDT",
|
||||
"ORDI/USDT:USDT",
|
||||
"AAVE/USDT:USDT",
|
||||
"REEF/USDT:USDT",
|
||||
"1000SATS/USDT:USDT",
|
||||
"SUI/USDT:USDT",
|
||||
"1INCH/USDT:USDT",
|
||||
"DOGE/USDT:USDT",
|
||||
"TON/USDT:USDT",
|
||||
"UNI/USDT:USDT",
|
||||
"XRP/USDT:USDT",
|
||||
"SUN/USDT:USDT",
|
||||
"NOT/USDT:USDT",
|
||||
"RARE/USDT:USDT",
|
||||
"RDNT/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 10,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8088,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "5m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "5985766683:AAEx2Nm_4y2IC0Tj4Hhz7djVRJRso0JKaj0",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8088,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.btc_chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8888,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"strategy": "ChanStrategy",
|
||||
"db_url": "sqlite:///tradesv3.btc_chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8818,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8815,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDC",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "0xA834b6d3Fa1D8A55ea8e502685ef5cbD2b2D3343",
|
||||
"privateKey": "0xa399cea4c01be67c16b88e1d2121ed6e72bab6b4e8d81684ed03ce78f8b4f827",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"PURR/USDC:USDC",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8815,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chan_sol_30.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8818,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chan.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8801,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"db_url": "sqlite:///tradesv3.deepseek_trader.sqlite",
|
||||
"dry_run": true,
|
||||
"dry_run_wallet": 10000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT",
|
||||
"ETH/USDT:USDT",
|
||||
"SOL/USDT:USDT",
|
||||
"WIF/USDT:USDT",
|
||||
"1000PEPE/USDT:USDT",
|
||||
"DOGS/USDT:USDT",
|
||||
"ORDI/USDT:USDT",
|
||||
"AAVE/USDT:USDT",
|
||||
"REEF/USDT:USDT",
|
||||
"1000SATS/USDT:USDT",
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 10,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "5985766683:AAEx2Nm_4y2IC0Tj4Hhz7djVRJRso0JKaj0",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8001,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": true,
|
||||
"internals": {
|
||||
"process_throttle_secs": 15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.ema26_ema52_cross.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8820,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.ema_pattern.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1h",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8888,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 2,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.elliottwave_btc.sqlite",
|
||||
"dry_run_wallet": 10000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"unfilledtimeout": {
|
||||
"entry": 5,
|
||||
"exit": 5,
|
||||
"exit_timeout_count": 3,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "",
|
||||
"chat_id": ""
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "freqtrade_secret",
|
||||
"ws_token": "freqtrade_ws",
|
||||
"username": "freqtrade",
|
||||
"password": "freqtrade"
|
||||
},
|
||||
"bot_name": "ElliottWaveBTC"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.freqai_sol.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short": true,
|
||||
"timeframe": "5m",
|
||||
"process_only_new_candles": true,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList"
|
||||
}
|
||||
],
|
||||
"freqai": {
|
||||
"enabled": true,
|
||||
"purge_old_models": 2,
|
||||
"train_period_days": 10,
|
||||
"backtest_period_days": 7,
|
||||
"live_retrain_hours": 1,
|
||||
"identifier": "sol_futures_lgbm_v1",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": [
|
||||
"5m",
|
||||
"15m"
|
||||
],
|
||||
"include_corr_pairlist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"label_period_candles": 12,
|
||||
"include_shifted_candles": 1,
|
||||
"DI_threshold": 0.9,
|
||||
"weight_factor": 0.9,
|
||||
"principal_component_analysis": false,
|
||||
"use_SVM_to_remove_outliers": true,
|
||||
"indicator_periods_candles": [
|
||||
14
|
||||
],
|
||||
"plot_feature_importances": 0
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.15,
|
||||
"random_state": 42
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 300,
|
||||
"learning_rate": 0.05,
|
||||
"max_depth": 5,
|
||||
"num_leaves": 31,
|
||||
"min_child_samples": 20,
|
||||
"subsample": 0.8,
|
||||
"colsample_bytree": 0.8,
|
||||
"reg_alpha": 0.1,
|
||||
"reg_lambda": 0.1,
|
||||
"n_jobs": 1,
|
||||
"verbosity": -1
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "",
|
||||
"chat_id": ""
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8822,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqai_sol",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.heikinashi_btc.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "1m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8814,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.ema26_ema52_cross.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"timeframe": "1m",
|
||||
"can_short" : true,
|
||||
"process_only_new_candles" : true,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "other",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "other",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8821,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.sol5m.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short": true,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "other",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing": {
|
||||
"price_side": "other",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": 8822,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "SOL5m",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": "unlimited",
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"can_short" : true,
|
||||
"timeframe" : "15m",
|
||||
"process_only_new_candles" : false,
|
||||
"unfilledtimeout": {
|
||||
"entry": 1,
|
||||
"exit": 1,
|
||||
"exit_timeout_count": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"WIF/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
"number_assets": 1,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||
"chat_id": "580807463"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": true,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8811,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||
"CORS_origins": [],
|
||||
"username": "freqtrader",
|
||||
"password": "FreqTrade007"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 2
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
"""crypto_wyckoff — multi-TF screener for crypto (ported from A_Share_DP Architecture v1.0)."""
|
||||
|
||||
from crypto_wyckoff.version import ARCHITECTURE_VERSION, WYCKOFF_ENGINE_VERSION
|
||||
|
||||
__all__ = ["WYCKOFF_ENGINE_VERSION", "ARCHITECTURE_VERSION"]
|
||||
@@ -0,0 +1,329 @@
|
||||
"""Walk-forward Wyckoff phase/event annotations for chart overlay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from crypto_wyckoff.domain_models import OHLCVFrame, WyckoffCycle, WyckoffEvent, WyckoffPhase
|
||||
from crypto_wyckoff.cycle import CycleEngine
|
||||
from crypto_wyckoff.event import EventEngine
|
||||
from crypto_wyckoff.features import FeatureEngine
|
||||
from crypto_wyckoff.phase import PhaseEngine
|
||||
|
||||
_MIN_BARS = {"1d": 40, "1w": 26, "1M": 18}
|
||||
|
||||
_NOTABLE_EVENTS = {
|
||||
WyckoffEvent.PS.value,
|
||||
WyckoffEvent.SC.value,
|
||||
WyckoffEvent.AR.value,
|
||||
WyckoffEvent.ST.value,
|
||||
WyckoffEvent.SPRING.value,
|
||||
WyckoffEvent.TEST.value,
|
||||
WyckoffEvent.SOS.value,
|
||||
WyckoffEvent.LPS.value,
|
||||
WyckoffEvent.JUMP.value,
|
||||
WyckoffEvent.BACKUP.value,
|
||||
WyckoffEvent.BC.value,
|
||||
WyckoffEvent.UTAD.value,
|
||||
WyckoffEvent.SOW.value,
|
||||
WyckoffEvent.LPSY.value,
|
||||
}
|
||||
|
||||
|
||||
def _slice_frame(frame: OHLCVFrame, end_idx: int) -> OHLCVFrame:
|
||||
n = end_idx + 1
|
||||
return OHLCVFrame(
|
||||
ts_code=frame.ts_code,
|
||||
timeframe=frame.timeframe,
|
||||
trade_dates=frame.trade_dates[:n],
|
||||
open=frame.open[:n],
|
||||
high=frame.high[:n],
|
||||
low=frame.low[:n],
|
||||
close=frame.close[:n],
|
||||
volume=frame.volume[:n],
|
||||
amount=frame.amount[:n] if frame.amount else [],
|
||||
)
|
||||
|
||||
|
||||
def _compress_phases(points: list[tuple[str, str]]) -> list[dict]:
|
||||
"""points: [(date_iso, phase), ...] → segments."""
|
||||
if not points:
|
||||
return []
|
||||
segs: list[dict] = []
|
||||
start, phase = points[0]
|
||||
prev = start
|
||||
for d, p in points[1:]:
|
||||
if p != phase:
|
||||
segs.append({"start": start, "end": prev, "phase": phase})
|
||||
start, phase = d, p
|
||||
prev = d
|
||||
segs.append({"start": start, "end": prev, "phase": phase})
|
||||
return segs
|
||||
|
||||
|
||||
def annotate_frame(frame: OHLCVFrame, step: int | None = None) -> dict:
|
||||
"""Pure annotation: phase bands + event markers + latest levels.
|
||||
|
||||
``step`` defaults by timeframe to keep interactive charts snappy.
|
||||
"""
|
||||
tf = frame.timeframe
|
||||
min_bars = _MIN_BARS.get(tf, 30)
|
||||
if step is None:
|
||||
step = {"1d": 2, "1w": 1, "1M": 1}.get(tf, 2)
|
||||
|
||||
empty = {
|
||||
"phases": [],
|
||||
"events": [],
|
||||
"levels": {},
|
||||
"bars": len(frame),
|
||||
"timeframe": tf,
|
||||
}
|
||||
if frame.empty or len(frame) < min_bars:
|
||||
return empty
|
||||
|
||||
feat_eng = FeatureEngine()
|
||||
cycle_eng = CycleEngine()
|
||||
phase_eng = PhaseEngine()
|
||||
event_eng = EventEngine()
|
||||
|
||||
phase_points: list[tuple[str, str]] = []
|
||||
events: list[dict] = []
|
||||
last_event: str | None = None
|
||||
levels: dict = {}
|
||||
|
||||
# Ensure last bar is always evaluated
|
||||
indices = list(range(min_bars - 1, len(frame), step))
|
||||
if indices[-1] != len(frame) - 1:
|
||||
indices.append(len(frame) - 1)
|
||||
|
||||
for i in indices:
|
||||
sub = _slice_frame(frame, i)
|
||||
f = feat_eng.run(sub, tf)
|
||||
c = cycle_eng.run(f, tf)
|
||||
p = phase_eng.run(c, f, tf)
|
||||
e = event_eng.run(c, p, f, tf)
|
||||
|
||||
d = str(frame.trade_dates[i])[:10]
|
||||
phase = p.payload.get("phase") or WyckoffPhase.NONE.value
|
||||
phase_points.append((d, phase))
|
||||
|
||||
cur = e.payload.get("current_event") or WyckoffEvent.NONE.value
|
||||
if cur in _NOTABLE_EVENTS and cur != last_event:
|
||||
events.append({
|
||||
"date": d,
|
||||
"event": cur,
|
||||
"price": float(frame.close[i]),
|
||||
"low": float(frame.low[i]),
|
||||
"high": float(frame.high[i]),
|
||||
})
|
||||
last_event = cur
|
||||
elif cur == WyckoffEvent.NONE.value:
|
||||
last_event = None
|
||||
|
||||
if i == len(frame) - 1 and not f.payload.get("insufficient"):
|
||||
levels = {
|
||||
k: f.payload.get(k)
|
||||
for k in (
|
||||
"range_high", "range_low", "ma20", "ma60",
|
||||
"swing_high", "swing_low", "close",
|
||||
)
|
||||
if f.payload.get(k) is not None
|
||||
}
|
||||
levels["phase"] = phase
|
||||
levels["cycle"] = c.payload.get("cycle")
|
||||
levels["current_event"] = cur
|
||||
|
||||
return {
|
||||
"phases": _compress_phases(phase_points),
|
||||
"events": events,
|
||||
"levels": levels,
|
||||
"bars": len(frame),
|
||||
"timeframe": tf,
|
||||
}
|
||||
|
||||
|
||||
_RANGE_CYCLES = {
|
||||
WyckoffCycle.ACCUMULATION.value,
|
||||
WyckoffCycle.RE_ACCUMULATION.value,
|
||||
WyckoffCycle.DISTRIBUTION.value,
|
||||
WyckoffCycle.RE_DISTRIBUTION.value,
|
||||
}
|
||||
|
||||
|
||||
def _build_range_zones(
|
||||
price_frame: OHLCVFrame,
|
||||
cycle_segs: list[dict],
|
||||
levels: dict | None = None,
|
||||
) -> list[dict]:
|
||||
"""Build price boxes (high/low × date span) for accum/distrib ranges."""
|
||||
if price_frame.empty:
|
||||
return []
|
||||
dates = [str(d)[:10] for d in price_frame.trade_dates]
|
||||
highs = price_frame.high
|
||||
lows = price_frame.low
|
||||
zones: list[dict] = []
|
||||
|
||||
for seg in cycle_segs or []:
|
||||
cy = seg.get("cycle")
|
||||
if cy not in _RANGE_CYCLES:
|
||||
continue
|
||||
start, end = seg["start"], seg["end"]
|
||||
idxs = [i for i, d in enumerate(dates) if start <= d <= end]
|
||||
if not idxs:
|
||||
# weekly bar date may sit between daily bars — take nearest window
|
||||
i0 = next((i for i, d in enumerate(dates) if d >= start), None)
|
||||
if i0 is None:
|
||||
continue
|
||||
i1 = next((i for i, d in enumerate(dates) if d > end), len(dates)) - 1
|
||||
idxs = list(range(i0, max(i0, i1) + 1))
|
||||
if not idxs:
|
||||
continue
|
||||
# pad short weekly hits to at least ~1 week of dailies for visibility
|
||||
if len(idxs) < 5 and idxs[-1] + 1 < len(dates):
|
||||
extra = min(5 - len(idxs), len(dates) - 1 - idxs[-1])
|
||||
idxs = list(range(idxs[0], idxs[-1] + 1 + max(0, extra)))
|
||||
hi = max(highs[i] for i in idxs)
|
||||
lo = min(lows[i] for i in idxs)
|
||||
if hi <= lo:
|
||||
continue
|
||||
zones.append({
|
||||
"kind": cy,
|
||||
"start": dates[idxs[0]],
|
||||
"end": dates[idxs[-1]],
|
||||
"high": float(hi),
|
||||
"low": float(lo),
|
||||
"current": False,
|
||||
})
|
||||
|
||||
# Always expose the latest trading-range box from feature snapshot
|
||||
levels = levels or {}
|
||||
rh, rl = levels.get("range_high"), levels.get("range_low")
|
||||
if rh is not None and rl is not None and float(rh) > float(rl):
|
||||
look = min(60, len(dates))
|
||||
cy = levels.get("cycle") or "Unknown"
|
||||
if cy not in _RANGE_CYCLES:
|
||||
# Phase B/C in a range → treat as accumulation-style TR for display
|
||||
ph = levels.get("phase") or ""
|
||||
if ph in ("A", "B", "C"):
|
||||
cy = WyckoffCycle.ACCUMULATION.value
|
||||
elif ph in ("D", "E") and float(levels.get("close") or 0) < float(rh):
|
||||
cy = WyckoffCycle.ACCUMULATION.value
|
||||
else:
|
||||
cy = "Range"
|
||||
zones.append({
|
||||
"kind": cy,
|
||||
"start": dates[-look],
|
||||
"end": dates[-1],
|
||||
"high": float(rh),
|
||||
"low": float(rl),
|
||||
"current": True,
|
||||
})
|
||||
|
||||
return zones
|
||||
|
||||
|
||||
def annotate_symbol(
|
||||
ts_code: str,
|
||||
freq: str,
|
||||
end_date: date | None = None,
|
||||
lookback: int = 180,
|
||||
) -> dict:
|
||||
"""IO + annotate for one symbol (used by API).
|
||||
|
||||
For daily charts, phase bands come from **weekly** structure (Wyckoff
|
||||
primary timeframe), while event markers / levels come from daily.
|
||||
"""
|
||||
from crypto_wyckoff.io import latest_daily_trade_date, load_frames_batch
|
||||
|
||||
if freq not in ("1d", "1w", "1M"):
|
||||
raise ValueError(f"unsupported freq: {freq}")
|
||||
ed = end_date or latest_daily_trade_date()
|
||||
empty = {
|
||||
"ts_code": ts_code,
|
||||
"freq": freq,
|
||||
"phases": [],
|
||||
"events": [],
|
||||
"levels": {},
|
||||
"zones": [],
|
||||
"bars": 0,
|
||||
"phase_source": freq,
|
||||
}
|
||||
if ed is None:
|
||||
return empty
|
||||
|
||||
if freq == "1d":
|
||||
daily_frames = load_frames_batch("1d", ed, lookback, ts_codes=[ts_code])
|
||||
weekly_frames = load_frames_batch("1w", ed, max(60, lookback // 3), ts_codes=[ts_code])
|
||||
daily = daily_frames.get(ts_code)
|
||||
weekly = weekly_frames.get(ts_code)
|
||||
if daily is None:
|
||||
return empty
|
||||
d_ann = annotate_frame(daily)
|
||||
w_ann = annotate_frame(weekly) if weekly is not None else {"phases": []}
|
||||
cycles = _cycle_segments(weekly) if weekly is not None else []
|
||||
levels = d_ann.get("levels") or {}
|
||||
# Prefer weekly cycle on the latest levels for zone labeling
|
||||
if cycles:
|
||||
levels = {**levels, "cycle": cycles[-1].get("cycle") or levels.get("cycle")}
|
||||
# latest non-None weekly phase
|
||||
for p in reversed(w_ann.get("phases") or []):
|
||||
if p.get("phase") not in (None, "None"):
|
||||
levels = {**levels, "phase": p["phase"]}
|
||||
break
|
||||
return {
|
||||
"ts_code": ts_code,
|
||||
"freq": freq,
|
||||
"end_date": ed.isoformat(),
|
||||
"phases": w_ann.get("phases") or [],
|
||||
"events": d_ann.get("events") or [],
|
||||
"levels": d_ann.get("levels") or {},
|
||||
"zones": _build_range_zones(daily, cycles, levels),
|
||||
"bars": d_ann.get("bars", 0),
|
||||
"phase_source": "1w",
|
||||
"cycles": cycles,
|
||||
}
|
||||
|
||||
frames = load_frames_batch(freq, ed, lookback, ts_codes=[ts_code])
|
||||
frame = frames.get(ts_code)
|
||||
if frame is None:
|
||||
return empty
|
||||
out = annotate_frame(frame)
|
||||
out["ts_code"] = ts_code
|
||||
out["freq"] = freq
|
||||
out["end_date"] = ed.isoformat()
|
||||
out["phase_source"] = freq
|
||||
out["cycles"] = _cycle_segments(frame)
|
||||
out["zones"] = _build_range_zones(frame, out["cycles"], out.get("levels") or {})
|
||||
if freq == "1M":
|
||||
# Monthly chart: cycle bands are more meaningful than phase
|
||||
if not any(p.get("phase") not in (None, "None") for p in out["phases"]):
|
||||
out["phases"] = [
|
||||
{"start": c["start"], "end": c["end"], "phase": c["cycle"]}
|
||||
for c in out["cycles"]
|
||||
if c.get("cycle") and c["cycle"] != "Unknown"
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
def _cycle_segments(frame: OHLCVFrame, step: int | None = None) -> list[dict]:
|
||||
"""Walk-forward cycle labels compressed to segments."""
|
||||
tf = frame.timeframe
|
||||
min_bars = _MIN_BARS.get(tf, 30)
|
||||
if step is None:
|
||||
step = {"1d": 3, "1w": 1, "1M": 1}.get(tf, 2)
|
||||
if frame.empty or len(frame) < min_bars:
|
||||
return []
|
||||
|
||||
feat_eng = FeatureEngine()
|
||||
cycle_eng = CycleEngine()
|
||||
points: list[tuple[str, str]] = []
|
||||
indices = list(range(min_bars - 1, len(frame), step))
|
||||
if indices[-1] != len(frame) - 1:
|
||||
indices.append(len(frame) - 1)
|
||||
for i in indices:
|
||||
sub = _slice_frame(frame, i)
|
||||
f = feat_eng.run(sub, tf)
|
||||
c = cycle_eng.run(f, tf)
|
||||
points.append((str(frame.trade_dates[i])[:10], c.payload.get("cycle") or "Unknown"))
|
||||
segs = _compress_phases(points)
|
||||
return [{"start": s["start"], "end": s["end"], "cycle": s["phase"]} for s in segs]
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Cycle Engine — monthly/weekly macro cycle via Rule Registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from crypto_wyckoff.domain_models import EngineResult, WyckoffCycle
|
||||
from crypto_wyckoff.rules.base import RuleHit
|
||||
from crypto_wyckoff.rules.registry import rule_registry
|
||||
|
||||
|
||||
def _resolve_range_conflict(hits: list[RuleHit], features: dict) -> list[RuleHit]:
|
||||
"""Accumulation vs Distribution overlap → mutually exclusive by MA120 position."""
|
||||
accum = [h for h in hits if h.cycle == WyckoffCycle.ACCUMULATION.value]
|
||||
dist = [h for h in hits if h.cycle == WyckoffCycle.DISTRIBUTION.value]
|
||||
if not (accum and dist):
|
||||
return hits
|
||||
|
||||
close = float(features.get("close") or 0)
|
||||
ma120 = float(features.get("ma120") or close) or close
|
||||
others = [
|
||||
h for h in hits
|
||||
if h.cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.DISTRIBUTION.value)
|
||||
]
|
||||
# Below MA120 → accumulation; above → distribution; equal band uses relative position
|
||||
if close < ma120 * 0.995:
|
||||
return others + accum
|
||||
if close > ma120 * 1.005:
|
||||
return others + dist
|
||||
# Tight band: keep higher confidence only
|
||||
best_a = max(accum, key=lambda h: h.confidence)
|
||||
best_d = max(dist, key=lambda h: h.confidence)
|
||||
return others + ([best_a] if best_a.confidence >= best_d.confidence else [best_d])
|
||||
|
||||
|
||||
class CycleEngine:
|
||||
name = "Cycle"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(self, feature: EngineResult, timeframe: str) -> EngineResult:
|
||||
features = feature.payload
|
||||
if features.get("insufficient"):
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=15.0,
|
||||
score=40.0,
|
||||
reasons=[f"{timeframe} 数据不足,Cycle=Unknown"],
|
||||
warnings=["insufficient_features"],
|
||||
payload={
|
||||
"cycle": WyckoffCycle.UNKNOWN.value,
|
||||
"timeframe": timeframe,
|
||||
"trend_score": 40.0,
|
||||
},
|
||||
)
|
||||
|
||||
context = {"features": features, "timeframe": timeframe}
|
||||
hits: list[RuleHit] = []
|
||||
for rule in rule_registry.by_category("cycle", timeframe):
|
||||
hit = rule.evaluate(context)
|
||||
if hit and hit.cycle:
|
||||
hits.append(hit)
|
||||
|
||||
hits = _resolve_range_conflict(hits, features)
|
||||
|
||||
if not hits:
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=30.0,
|
||||
score=40.0,
|
||||
reasons=["无匹配周期规则,标记 Unknown"],
|
||||
payload={
|
||||
"cycle": WyckoffCycle.UNKNOWN.value,
|
||||
"timeframe": timeframe,
|
||||
"trend_score": 40.0,
|
||||
},
|
||||
)
|
||||
|
||||
best = max(hits, key=lambda h: h.confidence)
|
||||
trend_score = best.score
|
||||
if best.cycle == WyckoffCycle.MARKUP.value:
|
||||
trend_score = max(trend_score, 75.0)
|
||||
elif best.cycle == WyckoffCycle.ACCUMULATION.value:
|
||||
trend_score = max(60.0, trend_score * 0.9)
|
||||
elif best.cycle == WyckoffCycle.DISTRIBUTION.value:
|
||||
trend_score = min(45.0, 100 - trend_score * 0.5)
|
||||
elif best.cycle == WyckoffCycle.MARKDOWN.value:
|
||||
trend_score = min(30.0, 100 - trend_score)
|
||||
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=best.confidence,
|
||||
score=trend_score,
|
||||
reasons=best.reasons,
|
||||
metrics=best.metrics,
|
||||
payload={
|
||||
"cycle": best.cycle,
|
||||
"timeframe": timeframe,
|
||||
"rule_id": best.rule_id,
|
||||
"trend_score": trend_score,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Decision Engine — multi-timeframe fusion and tradability (Architecture v1.0)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from crypto_wyckoff.domain_models import (
|
||||
DecisionSignal,
|
||||
EngineResult,
|
||||
RiskLevel,
|
||||
WyckoffCycle,
|
||||
WyckoffEvent,
|
||||
WyckoffPhase,
|
||||
)
|
||||
|
||||
BULL_CYCLES = {
|
||||
WyckoffCycle.ACCUMULATION.value,
|
||||
WyckoffCycle.RE_ACCUMULATION.value,
|
||||
WyckoffCycle.MARKUP.value,
|
||||
}
|
||||
BEAR_CYCLES = {
|
||||
WyckoffCycle.DISTRIBUTION.value,
|
||||
WyckoffCycle.RE_DISTRIBUTION.value,
|
||||
WyckoffCycle.MARKDOWN.value,
|
||||
}
|
||||
|
||||
|
||||
class DecisionEngine:
|
||||
name = "Decision"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(
|
||||
self,
|
||||
monthly_cycle: EngineResult,
|
||||
weekly_cycle: EngineResult,
|
||||
weekly_phase: EngineResult,
|
||||
weekly_event: EngineResult,
|
||||
daily_event: EngineResult,
|
||||
daily_signal: EngineResult,
|
||||
) -> EngineResult:
|
||||
m_cycle = monthly_cycle.payload.get("cycle", WyckoffCycle.UNKNOWN.value)
|
||||
w_cycle = weekly_cycle.payload.get("cycle", WyckoffCycle.UNKNOWN.value)
|
||||
w_phase = weekly_phase.payload.get("phase", WyckoffPhase.NONE.value)
|
||||
w_event = weekly_event.payload.get("current_event", WyckoffEvent.NONE.value)
|
||||
d_event = daily_event.payload.get("current_event", WyckoffEvent.NONE.value)
|
||||
|
||||
trend_score = float(monthly_cycle.payload.get("trend_score", monthly_cycle.score))
|
||||
structure_score = float(weekly_phase.payload.get("structure_score", weekly_phase.score))
|
||||
entry_score = float(daily_event.payload.get("entry_score", daily_event.score))
|
||||
|
||||
overall_score = 0.30 * trend_score + 0.30 * structure_score + 0.40 * entry_score
|
||||
|
||||
reasons: list[str] = []
|
||||
warnings: list[str] = []
|
||||
alignment = 50.0
|
||||
|
||||
m_bull = m_cycle in BULL_CYCLES
|
||||
m_bear = m_cycle in BEAR_CYCLES
|
||||
w_bull = w_cycle in BULL_CYCLES
|
||||
d_bullish_event = d_event in {
|
||||
WyckoffEvent.SPRING.value,
|
||||
WyckoffEvent.TEST.value,
|
||||
WyckoffEvent.SOS.value,
|
||||
WyckoffEvent.LPS.value,
|
||||
WyckoffEvent.JUMP.value,
|
||||
WyckoffEvent.BACKUP.value,
|
||||
}
|
||||
d_bearish_event = d_event in {
|
||||
WyckoffEvent.UTAD.value,
|
||||
WyckoffEvent.SOW.value,
|
||||
WyckoffEvent.LPSY.value,
|
||||
}
|
||||
|
||||
# Alignment scoring
|
||||
if m_bull and w_bull and d_bullish_event:
|
||||
alignment = 92.0
|
||||
reasons.append("✓ 月/周多头结构与日线多头事件一致")
|
||||
elif m_bull and d_bullish_event:
|
||||
alignment = 78.0
|
||||
reasons.append("✓ 月线支持,日线有入场事件")
|
||||
if not w_bull:
|
||||
warnings.append("周线结构未完全确认")
|
||||
alignment -= 8
|
||||
elif m_bear and d_bullish_event:
|
||||
alignment = 35.0
|
||||
reasons.append("✗ 月线派发/下跌,日线弹簧可能只是反弹")
|
||||
elif m_bear and d_bearish_event:
|
||||
alignment = 85.0
|
||||
reasons.append("✓ 空头多周期一致")
|
||||
else:
|
||||
alignment = 55.0
|
||||
reasons.append("○ 多周期部分一致,需观察")
|
||||
|
||||
if w_phase in (WyckoffPhase.D.value, WyckoffPhase.E.value) and m_bull:
|
||||
alignment = min(98.0, alignment + 6)
|
||||
reasons.append(f"✓ 周线阶段 {w_phase} 结构成熟({w_event})")
|
||||
active = daily_event.payload.get("active_events") or daily_event.payload.get("recent_events") or []
|
||||
if d_event == WyckoffEvent.SPRING.value and len(active) >= 3:
|
||||
alignment = min(98.0, alignment + 4)
|
||||
reasons.append("✓ 日线多重事件同时确认")
|
||||
|
||||
# Decision signal — hard gate on monthly bear + daily spring
|
||||
decision = DecisionSignal.WATCH.value
|
||||
risk = RiskLevel.MEDIUM.value
|
||||
|
||||
if m_bear and d_event == WyckoffEvent.SPRING.value:
|
||||
decision = DecisionSignal.WATCH.value
|
||||
risk = RiskLevel.HIGH.value
|
||||
overall_score = min(overall_score, 55.0)
|
||||
reasons.append("→ 决策:观察(月线不支持,禁止追日线弹簧)")
|
||||
elif m_bear and d_bullish_event:
|
||||
decision = DecisionSignal.AVOID.value
|
||||
risk = RiskLevel.HIGH.value
|
||||
overall_score = min(overall_score, 48.0)
|
||||
reasons.append("→ 决策:回避(逆大周期多头事件)")
|
||||
elif (
|
||||
m_bull
|
||||
and w_phase in (WyckoffPhase.D.value, WyckoffPhase.E.value, WyckoffPhase.C.value)
|
||||
and d_event in (WyckoffEvent.SPRING.value, WyckoffEvent.LPS.value, WyckoffEvent.SOS.value)
|
||||
and alignment >= 85
|
||||
and overall_score >= 80
|
||||
):
|
||||
decision = DecisionSignal.STRONG_BUY.value
|
||||
risk = RiskLevel.LOW.value
|
||||
reasons.append("→ 决策:强烈买入(三级共振)")
|
||||
elif m_bull and d_bullish_event and overall_score >= 68 and alignment >= 70:
|
||||
decision = DecisionSignal.BUY.value
|
||||
risk = RiskLevel.LOW.value if alignment >= 80 else RiskLevel.MEDIUM.value
|
||||
reasons.append("→ 决策:买入")
|
||||
elif m_bear and d_bearish_event and overall_score >= 65:
|
||||
decision = DecisionSignal.SELL.value
|
||||
risk = RiskLevel.MEDIUM.value
|
||||
reasons.append("→ 决策:卖出")
|
||||
else:
|
||||
decision = DecisionSignal.WATCH.value
|
||||
reasons.append("→ 决策:观察")
|
||||
|
||||
# Stars from score + alignment
|
||||
combo = 0.6 * overall_score + 0.4 * alignment
|
||||
if combo >= 90:
|
||||
stars = 5
|
||||
elif combo >= 80:
|
||||
stars = 4
|
||||
elif combo >= 65:
|
||||
stars = 3
|
||||
elif combo >= 50:
|
||||
stars = 2
|
||||
else:
|
||||
stars = 1
|
||||
|
||||
overall_confidence = (
|
||||
0.25 * monthly_cycle.confidence
|
||||
+ 0.25 * weekly_phase.confidence
|
||||
+ 0.25 * daily_event.confidence
|
||||
+ 0.25 * daily_signal.confidence
|
||||
)
|
||||
# Weak event pulls overall down
|
||||
if daily_event.confidence < 60:
|
||||
overall_confidence = min(overall_confidence, daily_event.confidence + 15)
|
||||
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=overall_confidence,
|
||||
score=overall_score,
|
||||
reasons=reasons,
|
||||
warnings=warnings,
|
||||
metrics={
|
||||
"trend_score": trend_score,
|
||||
"structure_score": structure_score,
|
||||
"entry_score": entry_score,
|
||||
"alignment": alignment,
|
||||
"stars": stars,
|
||||
},
|
||||
payload={
|
||||
"decision_signal": decision,
|
||||
"alignment": alignment,
|
||||
"stars": stars,
|
||||
"risk": risk,
|
||||
"overall_score": overall_score,
|
||||
"overall_confidence": overall_confidence,
|
||||
"trend_score": trend_score,
|
||||
"structure_score": structure_score,
|
||||
"entry_score": entry_score,
|
||||
"m_cycle": m_cycle,
|
||||
"w_cycle": w_cycle,
|
||||
"w_phase": w_phase,
|
||||
"w_event": w_event,
|
||||
"d_event": d_event,
|
||||
# Facts preserved — never overwritten
|
||||
"facts": {
|
||||
"monthly": {"cycle": m_cycle},
|
||||
"weekly": {"cycle": w_cycle, "phase": w_phase, "event": w_event},
|
||||
"daily": {"event": d_event},
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Wyckoff Screener domain models — Architecture v1.0 frozen contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class WyckoffCycle(str, Enum):
|
||||
ACCUMULATION = "Accumulation"
|
||||
RE_ACCUMULATION = "ReAccumulation"
|
||||
MARKUP = "Markup"
|
||||
DISTRIBUTION = "Distribution"
|
||||
RE_DISTRIBUTION = "ReDistribution"
|
||||
MARKDOWN = "Markdown"
|
||||
UNKNOWN = "Unknown"
|
||||
|
||||
|
||||
class WyckoffPhase(str, Enum):
|
||||
A = "A"
|
||||
B = "B"
|
||||
C = "C"
|
||||
D = "D"
|
||||
E = "E"
|
||||
NONE = "None"
|
||||
|
||||
|
||||
class WyckoffEvent(str, Enum):
|
||||
PS = "PS"
|
||||
SC = "SC"
|
||||
AR = "AR"
|
||||
ST = "ST"
|
||||
SPRING = "Spring"
|
||||
TEST = "Test"
|
||||
SOS = "SOS"
|
||||
LPS = "LPS"
|
||||
JUMP = "Jump"
|
||||
BACKUP = "Backup"
|
||||
BC = "BC"
|
||||
UTAD = "UTAD"
|
||||
SOW = "SOW"
|
||||
LPSY = "LPSY"
|
||||
NONE = "None"
|
||||
|
||||
|
||||
class DecisionSignal(str, Enum):
|
||||
STRONG_BUY = "StrongBuy"
|
||||
BUY = "Buy"
|
||||
WATCH = "Watch"
|
||||
AVOID = "Avoid"
|
||||
SELL = "Sell"
|
||||
|
||||
|
||||
class RiskLevel(str, Enum):
|
||||
LOW = "Low"
|
||||
MEDIUM = "Medium"
|
||||
HIGH = "High"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EngineResult:
|
||||
"""Unified result envelope for every Wyckoff engine (v1.0 contract)."""
|
||||
|
||||
name: str
|
||||
version: str = "1.0.0"
|
||||
confidence: float = 0.0
|
||||
score: float = 0.0
|
||||
reasons: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
metrics: dict[str, Any] = field(default_factory=dict)
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
"confidence": self.confidence,
|
||||
"score": self.score,
|
||||
"reasons": self.reasons,
|
||||
"warnings": self.warnings,
|
||||
"metrics": self.metrics,
|
||||
"payload": self.payload,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OHLCVFrame:
|
||||
"""In-memory OHLCV for one symbol one timeframe. Engines never touch DB."""
|
||||
|
||||
ts_code: str
|
||||
timeframe: str # "1d" | "1w" | "1M"
|
||||
trade_dates: list[date]
|
||||
open: list[float]
|
||||
high: list[float]
|
||||
low: list[float]
|
||||
close: list[float]
|
||||
volume: list[float]
|
||||
amount: list[float] = field(default_factory=list)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.close)
|
||||
|
||||
@property
|
||||
def empty(self) -> bool:
|
||||
return len(self.close) == 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class WyckoffScanRow:
|
||||
"""Persisted scan row for wyckoff_scan table."""
|
||||
|
||||
trade_date: date
|
||||
ts_code: str
|
||||
name: str = ""
|
||||
industry: str = ""
|
||||
engine_version: str = "v1.0.0"
|
||||
|
||||
m_cycle: str = WyckoffCycle.UNKNOWN.value
|
||||
cycle_confidence: float = 0.0
|
||||
trend_score: float = 0.0
|
||||
|
||||
w_cycle: str = WyckoffCycle.UNKNOWN.value
|
||||
w_phase: str = WyckoffPhase.NONE.value
|
||||
w_current_event: str = WyckoffEvent.NONE.value
|
||||
w_recent_events_json: str = "[]"
|
||||
phase_confidence: float = 0.0
|
||||
structure_score: float = 0.0
|
||||
|
||||
d_current_event: str = WyckoffEvent.NONE.value
|
||||
d_recent_events_json: str = "[]"
|
||||
event_confidence: float = 0.0
|
||||
entry_score: float = 0.0
|
||||
|
||||
entry: Optional[float] = None
|
||||
stop: Optional[float] = None
|
||||
target1: Optional[float] = None
|
||||
target2: Optional[float] = None
|
||||
rr: Optional[float] = None
|
||||
|
||||
alignment: float = 0.0
|
||||
stars: int = 1
|
||||
decision_signal: str = DecisionSignal.WATCH.value
|
||||
signal_confidence: float = 0.0
|
||||
overall_confidence: float = 0.0
|
||||
overall_score: float = 0.0
|
||||
risk: str = RiskLevel.MEDIUM.value
|
||||
reasons_json: str = "[]"
|
||||
|
||||
feature_snapshot_json: str = "{}"
|
||||
markers_json: str = "[]"
|
||||
scanned_at: datetime = field(default_factory=datetime.now)
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Event Engine — active concurrent events via Rule Registry.
|
||||
|
||||
Note: `active_events` are rules that fire on the latest bar snapshot,
|
||||
NOT a historical SC→AR→ST timeline. Do not present as chronological chain.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from crypto_wyckoff.domain_models import EngineResult, WyckoffEvent
|
||||
from crypto_wyckoff.rules.registry import rule_registry
|
||||
|
||||
# Display order only (not temporal history)
|
||||
_DISPLAY_ORDER = [
|
||||
WyckoffEvent.PS.value,
|
||||
WyckoffEvent.SC.value,
|
||||
WyckoffEvent.AR.value,
|
||||
WyckoffEvent.ST.value,
|
||||
WyckoffEvent.SPRING.value,
|
||||
WyckoffEvent.TEST.value,
|
||||
WyckoffEvent.SOS.value,
|
||||
WyckoffEvent.LPS.value,
|
||||
WyckoffEvent.JUMP.value,
|
||||
WyckoffEvent.BACKUP.value,
|
||||
WyckoffEvent.BC.value,
|
||||
WyckoffEvent.UTAD.value,
|
||||
WyckoffEvent.SOW.value,
|
||||
WyckoffEvent.LPSY.value,
|
||||
]
|
||||
|
||||
# Dominant event: highest confidence wins; ties broken by this priority
|
||||
_DOMINANCE_PRIORITY = [
|
||||
WyckoffEvent.SOS.value,
|
||||
WyckoffEvent.LPS.value,
|
||||
WyckoffEvent.UTAD.value,
|
||||
WyckoffEvent.SPRING.value,
|
||||
WyckoffEvent.JUMP.value,
|
||||
WyckoffEvent.BACKUP.value,
|
||||
WyckoffEvent.TEST.value,
|
||||
WyckoffEvent.SC.value,
|
||||
WyckoffEvent.SOW.value,
|
||||
WyckoffEvent.AR.value,
|
||||
WyckoffEvent.ST.value,
|
||||
]
|
||||
|
||||
|
||||
class EventEngine:
|
||||
name = "Event"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(
|
||||
self,
|
||||
cycle: EngineResult,
|
||||
phase: EngineResult,
|
||||
feature: EngineResult,
|
||||
timeframe: str,
|
||||
) -> EngineResult:
|
||||
if feature.payload.get("insufficient"):
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=20.0,
|
||||
score=30.0,
|
||||
reasons=["特征不足,跳过事件识别"],
|
||||
warnings=["insufficient_features"],
|
||||
payload={
|
||||
"current_event": WyckoffEvent.NONE.value,
|
||||
"active_events": [],
|
||||
"recent_events": [], # alias for DB/API compat; same as active_events
|
||||
"timeframe": timeframe,
|
||||
"entry_score": 30.0,
|
||||
},
|
||||
)
|
||||
|
||||
context = {
|
||||
"features": feature.payload,
|
||||
"cycle": cycle.payload,
|
||||
"phase": phase.payload,
|
||||
"timeframe": timeframe,
|
||||
}
|
||||
hits = []
|
||||
for rule in rule_registry.by_category("event", timeframe):
|
||||
hit = rule.evaluate(context)
|
||||
if hit and hit.event:
|
||||
hits.append(hit)
|
||||
|
||||
if not hits:
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=35.0,
|
||||
score=40.0,
|
||||
reasons=["无显著事件"],
|
||||
payload={
|
||||
"current_event": WyckoffEvent.NONE.value,
|
||||
"active_events": [],
|
||||
"recent_events": [],
|
||||
"timeframe": timeframe,
|
||||
"entry_score": 40.0,
|
||||
},
|
||||
)
|
||||
|
||||
by_event: dict[str, float] = {}
|
||||
reasons: list[str] = []
|
||||
metrics: dict = {}
|
||||
for h in hits:
|
||||
prev = by_event.get(h.event, -1.0)
|
||||
if h.confidence >= prev:
|
||||
by_event[h.event] = h.confidence
|
||||
reasons.extend(h.reasons)
|
||||
metrics.update(h.metrics)
|
||||
|
||||
active = [e for e in _DISPLAY_ORDER if e in by_event]
|
||||
for e in by_event:
|
||||
if e not in active:
|
||||
active.append(e)
|
||||
|
||||
# Dominant = max confidence; tie-break by dominance priority index
|
||||
def _dom_key(ev: str) -> tuple:
|
||||
conf = by_event[ev]
|
||||
try:
|
||||
prio = _DOMINANCE_PRIORITY.index(ev)
|
||||
except ValueError:
|
||||
prio = 99
|
||||
return (conf, -prio)
|
||||
|
||||
current = max(by_event.keys(), key=_dom_key)
|
||||
event_conf = by_event[current]
|
||||
co_bonus = min(12.0, max(0, len(active) - 1) * 3)
|
||||
entry_score = min(98.0, event_conf + co_bonus)
|
||||
if current == WyckoffEvent.SPRING.value and WyckoffEvent.TEST.value in by_event:
|
||||
entry_score = min(98.0, entry_score + 5)
|
||||
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=event_conf,
|
||||
score=entry_score,
|
||||
reasons=list(dict.fromkeys(reasons))[:8],
|
||||
warnings=["active_events_are_concurrent_not_timeline"],
|
||||
metrics=metrics,
|
||||
payload={
|
||||
"current_event": current,
|
||||
"active_events": active,
|
||||
"recent_events": active, # persisted column name; semantic = active
|
||||
"event_scores": by_event,
|
||||
"timeframe": timeframe,
|
||||
"entry_score": entry_score,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Feature Engine — pure function over OHLCVFrame → EngineResult(FeatureSnapshot)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from crypto_wyckoff.domain_models import EngineResult, OHLCVFrame
|
||||
|
||||
|
||||
def _sma(arr: np.ndarray, n: int) -> float:
|
||||
if len(arr) < n:
|
||||
return float(arr[-1]) if len(arr) else 0.0
|
||||
return float(np.mean(arr[-n:]))
|
||||
|
||||
|
||||
def _atr(high: np.ndarray, low: np.ndarray, close: np.ndarray, n: int = 14) -> float:
|
||||
if len(close) < 2:
|
||||
return 0.0
|
||||
prev_close = close[:-1]
|
||||
tr = np.maximum(high[1:] - low[1:], np.maximum(np.abs(high[1:] - prev_close), np.abs(low[1:] - prev_close)))
|
||||
if len(tr) < n:
|
||||
return float(np.mean(tr)) if len(tr) else 0.0
|
||||
return float(np.mean(tr[-n:]))
|
||||
|
||||
|
||||
def _adx(high: np.ndarray, low: np.ndarray, close: np.ndarray, n: int = 14) -> float:
|
||||
"""Simplified ADX approximation."""
|
||||
if len(close) < n + 2:
|
||||
return 15.0
|
||||
up = high[1:] - high[:-1]
|
||||
down = low[:-1] - low[1:]
|
||||
plus_dm = np.where((up > down) & (up > 0), up, 0.0)
|
||||
minus_dm = np.where((down > up) & (down > 0), down, 0.0)
|
||||
tr = np.maximum(high[1:] - low[1:], np.maximum(np.abs(high[1:] - close[:-1]), np.abs(low[1:] - close[:-1])))
|
||||
atr = np.mean(tr[-n:]) or 1e-9
|
||||
plus_di = 100 * np.mean(plus_dm[-n:]) / atr
|
||||
minus_di = 100 * np.mean(minus_dm[-n:]) / atr
|
||||
denom = plus_di + minus_di
|
||||
if denom < 1e-9:
|
||||
return 10.0
|
||||
dx = 100 * abs(plus_di - minus_di) / denom
|
||||
return float(min(60.0, dx))
|
||||
|
||||
|
||||
def compute_feature_snapshot(frame: OHLCVFrame) -> dict[str, Any]:
|
||||
"""Compute technical snapshot dict from OHLCV (no I/O)."""
|
||||
if frame.empty or len(frame) < 5:
|
||||
return {"ts_code": frame.ts_code, "timeframe": frame.timeframe, "bars": len(frame)}
|
||||
|
||||
close = np.asarray(frame.close, dtype=float)
|
||||
high = np.asarray(frame.high, dtype=float)
|
||||
low = np.asarray(frame.low, dtype=float)
|
||||
volume = np.asarray(frame.volume, dtype=float)
|
||||
open_ = np.asarray(frame.open, dtype=float)
|
||||
|
||||
ma20 = _sma(close, 20)
|
||||
ma60 = _sma(close, 60)
|
||||
ma120 = _sma(close, min(120, len(close)))
|
||||
atr = _atr(high, low, close, 14)
|
||||
vol_ma20 = _sma(volume, 20) or 1e-9
|
||||
volume_ratio = float(volume[-1] / vol_ma20)
|
||||
|
||||
look = min(60, len(close))
|
||||
window_h = high[-look:]
|
||||
window_l = low[-look:]
|
||||
range_high = float(np.max(window_h))
|
||||
range_low = float(np.min(window_l))
|
||||
rng = max(range_high - range_low, 1e-9)
|
||||
range_pct_60 = float(rng / close[-1]) if close[-1] else 0.0
|
||||
range_position = float((close[-1] - range_low) / rng)
|
||||
|
||||
# Spring / UTAD hints
|
||||
pierce_below = max(0.0, (range_low - low[-1]) / close[-1]) if close[-1] else 0.0
|
||||
# if previous bars broke below and last close back in range
|
||||
prior_low = float(np.min(low[-6:-1])) if len(low) >= 6 else float(low[-2])
|
||||
pierce_below = max(pierce_below, max(0.0, (range_low - prior_low) / close[-1]))
|
||||
close_back_in_range = 1.0 if close[-1] >= range_low else 0.0
|
||||
reclaim_speed = 0.0
|
||||
if pierce_below > 0 and close[-1] >= range_low:
|
||||
reclaim_speed = min(1.0, (close[-1] - low[-1]) / max(atr, 1e-9) / 2)
|
||||
|
||||
pierce_above = max(0.0, (high[-1] - range_high) / close[-1])
|
||||
fail_back = 1.0 if pierce_above > 0 and close[-1] <= range_high else 0.0
|
||||
breakout_above = 1.0 if close[-1] > range_high and volume_ratio >= 1.0 else -1.0
|
||||
|
||||
# pullback hold: close near ma20 from above after being higher
|
||||
pullback_hold = 0.0
|
||||
if len(close) >= 5 and close[-1] > ma20 and close[-3] > close[-1] and (close[-1] - ma20) / max(atr, 1e-9) < 1.5:
|
||||
pullback_hold = 0.8
|
||||
|
||||
ma60_prev = _sma(close[:-5], 60) if len(close) > 65 else ma60
|
||||
ma60_slope = (ma60 - ma60_prev) / max(abs(ma60_prev), 1e-9)
|
||||
|
||||
# volume trend: recent 10 vs prior 10
|
||||
if len(volume) >= 20:
|
||||
volume_trend = float(np.mean(volume[-10:]) / (np.mean(volume[-20:-10]) + 1e-9) - 1.0)
|
||||
else:
|
||||
volume_trend = 0.0
|
||||
|
||||
bar_range_atr = float((high[-1] - low[-1]) / max(atr, 1e-9))
|
||||
bounce_from_low = float((close[-1] - float(np.min(low[-10:]))) / close[-1]) if close[-1] else 0.0
|
||||
gap_up_pct = float((open_[-1] - close[-2]) / close[-2]) if len(close) >= 2 and close[-2] else 0.0
|
||||
after_strength = 0.0
|
||||
if len(close) >= 4 and close[-3] > close[-4]:
|
||||
after_strength = 0.7
|
||||
|
||||
spring_score_hint = 0.0
|
||||
if pierce_below >= 0.002 and close_back_in_range:
|
||||
spring_score_hint = min(90.0, 50 + pierce_below * 1500 + reclaim_speed * 20)
|
||||
utad_score_hint = min(90.0, 50 + pierce_above * 1500) if pierce_above >= 0.002 and fail_back else 0.0
|
||||
|
||||
# swing
|
||||
swing_high = float(np.max(high[-20:])) if len(high) >= 5 else float(high[-1])
|
||||
swing_low = float(np.min(low[-20:])) if len(low) >= 5 else float(low[-1])
|
||||
|
||||
return {
|
||||
"ts_code": frame.ts_code,
|
||||
"timeframe": frame.timeframe,
|
||||
"bars": len(frame),
|
||||
"close": float(close[-1]),
|
||||
"open": float(open_[-1]),
|
||||
"high": float(high[-1]),
|
||||
"low": float(low[-1]),
|
||||
"volume": float(volume[-1]),
|
||||
"ma20": ma20,
|
||||
"ma60": ma60,
|
||||
"ma120": ma120,
|
||||
"ma60_slope": float(ma60_slope),
|
||||
"atr": atr,
|
||||
"adx": _adx(high, low, close),
|
||||
"volume_ma20": float(vol_ma20),
|
||||
"volume_ratio": volume_ratio,
|
||||
"volume_trend": volume_trend,
|
||||
"range_high": range_high,
|
||||
"range_low": range_low,
|
||||
"range_pct_60": range_pct_60,
|
||||
"range_position": range_position,
|
||||
"pierce_below_range": pierce_below,
|
||||
"pierce_above_range": pierce_above,
|
||||
"close_back_in_range": close_back_in_range,
|
||||
"reclaim_speed": reclaim_speed,
|
||||
"fail_back_into_range": fail_back,
|
||||
"breakout_above_range": breakout_above,
|
||||
"pullback_hold": pullback_hold,
|
||||
"bar_range_atr": bar_range_atr,
|
||||
"bounce_from_low": bounce_from_low,
|
||||
"gap_up_pct": gap_up_pct,
|
||||
"after_strength": after_strength,
|
||||
"spring_score_hint": spring_score_hint,
|
||||
"utad_score_hint": utad_score_hint,
|
||||
"swing_high": swing_high,
|
||||
"swing_low": swing_low,
|
||||
"trade_date": str(frame.trade_dates[-1]) if frame.trade_dates else None,
|
||||
}
|
||||
|
||||
|
||||
# Minimum bars before a timeframe is considered usable (no cross-TF borrow)
|
||||
_MIN_BARS = {"1d": 40, "1w": 26, "1M": 18}
|
||||
|
||||
|
||||
class FeatureEngine:
|
||||
"""Pure Feature Engine — no database access."""
|
||||
|
||||
name = "Feature"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(self, frame: OHLCVFrame | None, timeframe: str | None = None) -> EngineResult:
|
||||
tf = timeframe or (frame.timeframe if frame else "1d")
|
||||
min_bars = _MIN_BARS.get(tf, 30)
|
||||
|
||||
if frame is None or frame.empty or len(frame) < min_bars:
|
||||
bars = 0 if frame is None or frame.empty else len(frame)
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=10.0,
|
||||
score=10.0,
|
||||
reasons=[f"{tf} bars={bars} < min={min_bars},标记 insufficient"],
|
||||
warnings=["insufficient_features"],
|
||||
metrics={"bars": bars, "min_bars": min_bars},
|
||||
payload={
|
||||
"ts_code": getattr(frame, "ts_code", ""),
|
||||
"timeframe": tf,
|
||||
"bars": bars,
|
||||
"insufficient": True,
|
||||
},
|
||||
)
|
||||
|
||||
snap = compute_feature_snapshot(frame)
|
||||
snap["insufficient"] = False
|
||||
conf = 90.0 if snap.get("bars", 0) >= 60 else 50.0 + min(40.0, snap.get("bars", 0) * 0.5)
|
||||
warnings = []
|
||||
if snap.get("bars", 0) < 60:
|
||||
warnings.append("bars偏少,特征可靠性中等")
|
||||
return EngineResult(
|
||||
name=self.name,
|
||||
version=self.version,
|
||||
confidence=conf,
|
||||
score=conf,
|
||||
reasons=[f"computed {snap.get('bars', 0)} bars {tf}"],
|
||||
warnings=warnings,
|
||||
metrics={"bars": snap.get("bars", 0)},
|
||||
payload=snap,
|
||||
)
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Paths + OHLCV cache + DATA_SERVICE fetch (crypto continuous calendar)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import requests
|
||||
|
||||
from crypto_wyckoff.domain_models import OHLCVFrame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
DATA_DIR = Path(os.environ.get("CRYPTO_WYCKOFF_DATA", str(_REPO_ROOT / "data" / "crypto_wyckoff")))
|
||||
BARS_DB = DATA_DIR / "bars.sqlite"
|
||||
SCAN_DB = DATA_DIR / "scan.sqlite"
|
||||
|
||||
DATA_SERVICE_URL = os.environ.get(
|
||||
"DATA_SERVICE_URL",
|
||||
os.environ.get("DATASVC_URL", "https://provider.jackyu66.com"),
|
||||
).rstrip("/")
|
||||
|
||||
# Continuous crypto: bar counts (not A-share weekend-padded calendar multipliers)
|
||||
# Provider has 1d/1w but no 1M — monthly is resampled locally from daily UTC months.
|
||||
LOOKBACK = {"1d": 250, "1w": 104, "1M": 60}
|
||||
TF_PROVIDER = ("1d", "1w")
|
||||
TF_LIST = ("1d", "1w", "1M")
|
||||
|
||||
|
||||
def ensure_dirs() -> None:
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _symbol_key(symbol: str) -> str:
|
||||
return symbol.replace("/", "_").replace(":", "_")
|
||||
|
||||
|
||||
def _bars_conn() -> sqlite3.Connection:
|
||||
ensure_dirs()
|
||||
conn = sqlite3.connect(str(BARS_DB), timeout=60)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS bars (
|
||||
symbol TEXT NOT NULL,
|
||||
tf TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
open REAL, high REAL, low REAL, close REAL, volume REAL,
|
||||
PRIMARY KEY (symbol, tf, ts)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bars_sym_tf ON bars(symbol, tf)")
|
||||
return conn
|
||||
|
||||
|
||||
def fetch_candles(
|
||||
symbol: str,
|
||||
tf: str,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
start_ms: int | None = None,
|
||||
end_ms: int | None = None,
|
||||
timeout: float = 15.0,
|
||||
) -> list[dict]:
|
||||
params: dict = {"symbol": symbol, "tf": tf}
|
||||
if limit is not None:
|
||||
params["limit"] = int(limit)
|
||||
if start_ms is not None:
|
||||
params["start"] = int(start_ms)
|
||||
if end_ms is not None:
|
||||
params["end"] = int(end_ms)
|
||||
resp = requests.get(f"{DATA_SERVICE_URL}/api/candles", params=params, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
out = []
|
||||
for row in data:
|
||||
try:
|
||||
ts = int(float(row["timestamp"]))
|
||||
out.append(
|
||||
{
|
||||
"ts": ts,
|
||||
"open": float(row["open"]),
|
||||
"high": float(row["high"]),
|
||||
"low": float(row["low"]),
|
||||
"close": float(row["close"]),
|
||||
"volume": float(row.get("volume") or 0),
|
||||
}
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
out.sort(key=lambda r: r["ts"])
|
||||
return out
|
||||
|
||||
|
||||
def upsert_bars(symbol: str, tf: str, rows: list[dict]) -> int:
|
||||
if not rows:
|
||||
return 0
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO bars(symbol, tf, ts, open, high, low, close, volume)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(symbol, tf, ts) DO UPDATE SET
|
||||
open=excluded.open, high=excluded.high, low=excluded.low,
|
||||
close=excluded.close, volume=excluded.volume
|
||||
""",
|
||||
[
|
||||
(symbol, tf, r["ts"], r["open"], r["high"], r["low"], r["close"], r["volume"])
|
||||
for r in rows
|
||||
],
|
||||
)
|
||||
conn.commit()
|
||||
return len(rows)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def load_frame(symbol: str, tf: str, lookback: int | None = None) -> OHLCVFrame | None:
|
||||
lookback = lookback or LOOKBACK.get(tf, 100)
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT ts, open, high, low, close, volume FROM bars
|
||||
WHERE symbol=? AND tf=?
|
||||
ORDER BY ts DESC LIMIT ?
|
||||
""",
|
||||
(symbol, tf, lookback),
|
||||
)
|
||||
rows = list(reversed(cur.fetchall()))
|
||||
finally:
|
||||
conn.close()
|
||||
if not rows:
|
||||
return None
|
||||
trade_dates: list[date] = []
|
||||
for ts, *_ in rows:
|
||||
trade_dates.append(datetime.fromtimestamp(ts / 1000.0, tz=timezone.utc).date())
|
||||
return OHLCVFrame(
|
||||
ts_code=symbol,
|
||||
timeframe=tf,
|
||||
trade_dates=trade_dates,
|
||||
open=[r[1] for r in rows],
|
||||
high=[r[2] for r in rows],
|
||||
low=[r[3] for r in rows],
|
||||
close=[r[4] for r in rows],
|
||||
volume=[r[5] for r in rows],
|
||||
)
|
||||
|
||||
|
||||
def bar_count(symbol: str, tf: str) -> int:
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"SELECT COUNT(*) FROM bars WHERE symbol=? AND tf=?", (symbol, tf)
|
||||
)
|
||||
return int(cur.fetchone()[0])
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def rebuild_monthly_from_daily(symbol: str) -> int:
|
||||
"""Aggregate UTC calendar-month OHLCV from local daily bars (provider has no 1M)."""
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT ts, open, high, low, close, volume FROM bars
|
||||
WHERE symbol=? AND tf='1d' ORDER BY ts ASC
|
||||
""",
|
||||
(symbol,),
|
||||
)
|
||||
daily = cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
if not daily:
|
||||
return 0
|
||||
|
||||
months: dict[tuple[int, int], dict] = {}
|
||||
for ts, o, h, l, c, v in daily:
|
||||
dt = datetime.fromtimestamp(ts / 1000.0, tz=timezone.utc)
|
||||
key = (dt.year, dt.month)
|
||||
# month bar open timestamp = first day 00:00 UTC
|
||||
month_ts = int(datetime(dt.year, dt.month, 1, tzinfo=timezone.utc).timestamp() * 1000)
|
||||
if key not in months:
|
||||
months[key] = {
|
||||
"ts": month_ts,
|
||||
"open": o,
|
||||
"high": h,
|
||||
"low": l,
|
||||
"close": c,
|
||||
"volume": v or 0.0,
|
||||
}
|
||||
else:
|
||||
m = months[key]
|
||||
m["high"] = max(m["high"], h)
|
||||
m["low"] = min(m["low"], l)
|
||||
m["close"] = c
|
||||
m["volume"] = (m["volume"] or 0) + (v or 0)
|
||||
|
||||
rows = sorted(months.values(), key=lambda r: r["ts"])
|
||||
# drop stale months then upsert
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
conn.execute("DELETE FROM bars WHERE symbol=? AND tf='1M'", (symbol,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return upsert_bars(symbol, "1M", rows)
|
||||
|
||||
|
||||
def backfill_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> dict:
|
||||
"""Pull history for continuous crypto TFs; monthly derived from daily."""
|
||||
stats = {}
|
||||
for tf in TF_PROVIDER:
|
||||
if tf not in tfs and "1M" not in tfs:
|
||||
continue
|
||||
need = LOOKBACK.get(tf, 100)
|
||||
# need extra daily for monthly history
|
||||
if tf == "1d":
|
||||
need = max(need, LOOKBACK["1M"] * 31)
|
||||
try:
|
||||
rows = fetch_candles(symbol, tf, limit=need)
|
||||
n = upsert_bars(symbol, tf, rows)
|
||||
stats[tf] = n
|
||||
except Exception as e:
|
||||
logger.warning("backfill %s %s failed: %s", symbol, tf, e)
|
||||
stats[tf] = 0
|
||||
time.sleep(0.05)
|
||||
if "1M" in tfs or True:
|
||||
try:
|
||||
stats["1M"] = rebuild_monthly_from_daily(symbol)
|
||||
except Exception as e:
|
||||
logger.warning("monthly rebuild %s failed: %s", symbol, e)
|
||||
stats["1M"] = 0
|
||||
return stats
|
||||
|
||||
|
||||
def tip_update_symbol(symbol: str, tfs: Iterable[str] = TF_LIST) -> bool:
|
||||
"""Update forming tip bars (limit=3). Returns True if any bar changed."""
|
||||
changed = False
|
||||
for tf in TF_PROVIDER:
|
||||
try:
|
||||
rows = fetch_candles(symbol, tf, limit=3)
|
||||
if not rows:
|
||||
continue
|
||||
before = _tip_fingerprint(symbol, tf)
|
||||
upsert_bars(symbol, tf, rows)
|
||||
after = _tip_fingerprint(symbol, tf)
|
||||
if before != after:
|
||||
changed = True
|
||||
except Exception as e:
|
||||
logger.debug("tip %s %s: %s", symbol, tf, e)
|
||||
time.sleep(0.02)
|
||||
# Always rebuild current month tip from daily
|
||||
before_m = _tip_fingerprint(symbol, "1M")
|
||||
try:
|
||||
rebuild_monthly_from_daily(symbol)
|
||||
except Exception as e:
|
||||
logger.debug("monthly tip %s: %s", symbol, e)
|
||||
after_m = _tip_fingerprint(symbol, "1M")
|
||||
if before_m != after_m:
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def _tip_fingerprint(symbol: str, tf: str) -> tuple | None:
|
||||
conn = _bars_conn()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT ts, open, high, low, close, volume FROM bars
|
||||
WHERE symbol=? AND tf=? ORDER BY ts DESC LIMIT 1
|
||||
""",
|
||||
(symbol, tf),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return tuple(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def fetch_symbols_from_provider() -> list[str]:
|
||||
try:
|
||||
resp = requests.get(f"{DATA_SERVICE_URL}/health", timeout=8)
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
symbols = payload.get("symbols") or payload.get("symbol_list") or []
|
||||
return [s for s in symbols if isinstance(s, str)]
|
||||
except Exception as e:
|
||||
logger.warning("health symbols failed: %s", e)
|
||||
return []
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user