refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块

删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-27 01:05:12 +08:00
co-authored by Cursor
parent 5c10e35b76
commit 7f393b93ed
360 changed files with 140008 additions and 41167 deletions
+8 -28
View File
@@ -1,42 +1,22 @@
# MacOS # MacOS
.DS_Store .DS_Store
# Python编译文件和缓存 # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
*.pyc *.pyc
*.pyo *.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 *.log
# Database files
*.sqlite *.sqlite
*.sqlite-shm *.sqlite-shm
*.sqlite-wal *.sqlite-wal
.DS_Store
交易记录/~$交易规则.docx # Local data
/datasvc/data data/
.DS_Store
.DS_Store # Local tooling
/data_provider/data
.DS_Store
.DS_Store
.DS_Store
.DS_Store
.DS_Store
.DS_Store
data_provider/._config.json
.gstack/ .gstack/
-33
View File
@@ -1,33 +0,0 @@
# 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`
-116
View File
@@ -1,116 +0,0 @@
# 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
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanBI import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanBIZS import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanBSP import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanCTime import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanEnum import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanHeng import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanKLC import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanKLU import * # noqa: F403
-3
View File
@@ -1,3 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.pipeline.orchestrator import ChanLun # noqa: F401
from chanlun.pipeline.timeframe import TF_DF # noqa: F401
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanLun_Classifier import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.indicators.ChanMACD import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.indicators.ChanMACDHistSet import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.indicators.ChanMACDSeg import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.indicators.ChanMACDUnitTF import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanPY import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanPivotClassifier import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanPivotMonitor import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanSBI import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanSEG import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.ChanZS import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.ChanZone import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.core.Chan_FX_Box import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.analysis.Find_Trend import * # noqa: F403
-2
View File
@@ -1,2 +0,0 @@
"""兼容 shim — 请优先 from chanlun import ..."""
from chanlun.pipeline.timeframe import TF_DF # noqa: F401
View File
-7
View File
@@ -1,7 +0,0 @@
"""威科夫分析(启发式):交易区间 / 阶段 / 事件 / 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"]
-196
View File
@@ -1,196 +0,0 @@
"""威科夫分析入口:Cycle → Phase → Event → VP + LiveMULTI-CYCLE / LIVE-STRUCTURE)。
range.py 只产 TradingRangeConfirmed 走 events.pyLive 走 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"),
}
-369
View File
@@ -1,369 +0,0 @@
"""威科夫阶段与事件(启发式)。"""
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
-258
View File
@@ -1,258 +0,0 @@
"""威科夫 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",
}
-442
View File
@@ -1,442 +0,0 @@
"""交易区间检测:仅负责 TradingRange(起止/高低/结构分)。
WYCKOFF-MULTI-CYCLE-001Phase/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_offsetslice 相对父 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
@@ -1,72 +0,0 @@
"""区间内 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,
}
+2 -2
View File
@@ -467,7 +467,7 @@ class BiBuilderMixin:
pre_last_bi = bi_list[-2] pre_last_bi = bi_list[-2]
last_bi = bi_list[-1] 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: 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) bi_list.remove(last_bi)
pre_last_bi.set_next(None) pre_last_bi.set_next(None)
#last_top.set_fx(Chan_FX_TYPE.PTOP) #last_top.set_fx(Chan_FX_TYPE.PTOP)
@@ -585,7 +585,7 @@ class BiBuilderMixin:
pre_last_bi = bi_list[-2] pre_last_bi = bi_list[-2]
last_bi = bi_list[-1] 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: 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) bi_list.remove(last_bi)
pre_last_bi.set_next(None) pre_last_bi.set_next(None)
last_bottom = klc last_bottom = klc
+15
View File
@@ -100,6 +100,21 @@ class KlineBuilderMixin:
return Chan_FX_TYPE.BOTTOM return Chan_FX_TYPE.BOTTOM
return Chan_FX_TYPE.UNKNOWN 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): def check_fx2(self, klc):
if klc.pre and klc.next: 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: if klc.high > klc.pre.close and klc.close > klc.next.close and klc.close > klc.pre.close and klc.close > klc.next.close:
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-98
View File
@@ -1,98 +0,0 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"strategy": "BTC_Maker_Micro_Scalper",
"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_maker_micro_scalper.sqlite",
"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": true,
"fee": 0.00016,
"unfilledtimeout": {
"entry": 1,
"exit": 1,
"exit_timeout_count": 3,
"unit": "minutes"
},
"order_types": {
"entry": "limit",
"exit": "limit",
"stoploss": "limit",
"stoploss_on_exchange": false
},
"order_time_in_force": {
"entry": "GTC",
"exit": "GTC"
},
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8821,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "change_me_mms_v1",
"ws_token": "change_me_mms_ws",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "BTC_Maker_Micro_Scalper",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 1
}
}
-98
View File
@@ -1,98 +0,0 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"strategy": "BTC_Maker_Micro_Scalper_v11",
"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_maker_micro_scalper_v11.sqlite",
"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": true,
"fee": 0.00016,
"unfilledtimeout": {
"entry": 3,
"exit": 2,
"exit_timeout_count": 3,
"unit": "minutes"
},
"order_types": {
"entry": "limit",
"exit": "limit",
"stoploss": "limit",
"stoploss_on_exchange": false
},
"order_time_in_force": {
"entry": "GTC",
"exit": "GTC"
},
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8822,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "change_me_mms_v11",
"ws_token": "change_me_mms_v11_ws",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "BTC_Maker_Micro_Scalper_v11",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 1
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-90
View File
@@ -1,90 +0,0 @@
{
"$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": {
"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": 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
}
}
-89
View File
@@ -1,89 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-68
View File
@@ -1,68 +0,0 @@
{
"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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-87
View File
@@ -1,87 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-151
View File
@@ -1,151 +0,0 @@
{
"$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"]
}
}
]
}
-125
View File
@@ -1,125 +0,0 @@
{
"$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
}
}
-103
View File
@@ -1,103 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-84
View File
@@ -1,84 +0,0 @@
{
"$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
}
}
-93
View File
@@ -1,93 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-70
View File
@@ -1,70 +0,0 @@
{
"$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"
}
-123
View File
@@ -1,123 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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
}
}
-91
View File
@@ -1,91 +0,0 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"strategy": "MakerEdgeProbe",
"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.maker_edge_probe.sqlite",
"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,
"fee": 0.00016,
"unfilledtimeout": {
"entry": 3,
"exit": 2,
"exit_timeout_count": 3,
"unit": "minutes"
},
"order_types": {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": false
},
"order_time_in_force": {
"entry": "GTC",
"exit": "GTC"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 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": {
"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"
}
],
"telegram": {
"enabled": false
},
"api_server": {
"enabled": true,
"listen_ip_address": "127.0.0.1",
"listen_port": 8823,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "maker_edge_probe_change_me",
"ws_token": "maker_edge_probe_ws",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "MakerEdgeProbe",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 2
}
}
-81
View File
@@ -1,81 +0,0 @@
{
"$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
}
}
-83
View File
@@ -1,83 +0,0 @@
{
"$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.
-86
View File
@@ -1,86 +0,0 @@
{
"$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.turtle_btc.sqlite",
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "15m",
"process_only_new_candles": true,
"unfilledtimeout": {
"entry": 15,
"exit": 15,
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8822,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "turtle-btc-change-me",
"ws_token": "turtle-btc-ws-change-me",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "turtle_btc",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}
-86
View File
@@ -1,86 +0,0 @@
{
"$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.wyckoff_btc.sqlite",
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "1h",
"process_only_new_candles": true,
"unfilledtimeout": {
"entry": 60,
"exit": 60,
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8823,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "wyckoff-btc-change-me",
"ws_token": "wyckoff-btc-ws-change-me",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "wyckoff_btc",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}
-86
View File
@@ -1,86 +0,0 @@
{
"$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.wyckoff_btc_gated.sqlite",
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "1h",
"process_only_new_candles": true,
"unfilledtimeout": {
"entry": 60,
"exit": 60,
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8825,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "wyckoff-gated-change-me",
"ws_token": "wyckoff-gated-ws-change-me",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "wyckoff_btc_gated",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}
-86
View File
@@ -1,86 +0,0 @@
{
"$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.wyckoff_btc_lps.sqlite",
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "1h",
"process_only_new_candles": true,
"unfilledtimeout": {
"entry": 60,
"exit": 60,
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8824,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "wyckoff-lps-change-me",
"ws_token": "wyckoff-lps-ws-change-me",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "wyckoff_btc_lps",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}
-86
View File
@@ -1,86 +0,0 @@
{
"$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.wyckoff_btc_v1_baseline.sqlite",
"dry_run_wallet": 10000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "1h",
"process_only_new_candles": true,
"unfilledtimeout": {
"entry": 60,
"exit": 60,
"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": {
"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"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8823,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "wyckoff-v1-baseline-change-me",
"ws_token": "wyckoff-v1-baseline-ws-change-me",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "wyckoff_btc_v1_baseline",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}
-5
View File
@@ -1,5 +0,0 @@
"""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"]
-342
View File
@@ -1,342 +0,0 @@
"""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,
*,
role: str | None = None,
) -> dict:
"""Pure annotation: phase bands + event markers + latest levels.
``role`` is the D/W/M rule alias (1d/1w/1M). Defaults to frame.timeframe.
``step`` defaults by role to keep interactive charts snappy.
"""
tf = role or 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,
*,
combo_id: str | None = None,
) -> dict:
"""IO + annotate for one symbol (used by API).
For the combo *low* chart, phase bands come from **mid** structure,
while event markers / levels come from the low TF.
"""
from crypto_wyckoff.combos import ROLE_HIGH, ROLE_LOW, ROLE_MID, get_combo
from crypto_wyckoff.io import load_frame
combo = get_combo(combo_id)
allowed = {combo["low"], combo["mid"], combo["high"]}
if freq not in allowed:
raise ValueError(f"freq {freq} not in combo {combo['id']} ({combo['label']})")
empty = {
"ts_code": ts_code,
"freq": freq,
"phases": [],
"events": [],
"levels": {},
"zones": [],
"bars": 0,
"phase_source": freq,
"cycles": [],
"combo_id": combo["id"],
}
_ = end_date
if freq == combo["low"]:
low = load_frame(ts_code, combo["low"], lookback)
mid = load_frame(ts_code, combo["mid"], max(60, lookback // 3))
if low is None:
return empty
d_ann = annotate_frame(low, role=ROLE_LOW)
w_ann = annotate_frame(mid, role=ROLE_MID) if mid is not None else {"phases": []}
cycles = _cycle_segments(mid, role=ROLE_MID) if mid is not None else []
levels = d_ann.get("levels") or {}
if cycles:
levels = {**levels, "cycle": cycles[-1].get("cycle") or levels.get("cycle")}
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": low.trade_dates[-1].isoformat() if low.trade_dates else None,
"phases": w_ann.get("phases") or [],
"events": d_ann.get("events") or [],
"levels": d_ann.get("levels") or {},
"zones": _build_range_zones(low, cycles, levels),
"bars": d_ann.get("bars", 0),
"phase_source": combo["mid"],
"cycles": cycles,
"combo_id": combo["id"],
}
role = ROLE_MID if freq == combo["mid"] else ROLE_HIGH
frame = load_frame(ts_code, freq, lookback)
if frame is None:
return empty
out = annotate_frame(frame, role=role)
out["ts_code"] = ts_code
out["freq"] = freq
out["end_date"] = frame.trade_dates[-1].isoformat() if frame.trade_dates else None
out["phase_source"] = freq
out["cycles"] = _cycle_segments(frame, role=ROLE_HIGH if role == ROLE_HIGH else ROLE_MID)
out["zones"] = _build_range_zones(frame, out["cycles"], out.get("levels") or {})
out["combo_id"] = combo["id"]
if role == ROLE_HIGH:
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,
*,
role: str | None = None,
) -> list[dict]:
"""Walk-forward cycle labels compressed to segments."""
tf = role or 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]
-248
View File
@@ -1,248 +0,0 @@
"""Multi-timeframe combo presets for Crypto Wyckoff Screener.
Roles (engine rule aliases stay D/W/M):
high Cycle (rules as 1M)
mid Phase (rules as 1w)
low Event (rules as 1d)
Actual bar TFs come from the combo (e.g. 8h/4h/1h).
"""
from __future__ import annotations
import json
import re
import threading
from copy import deepcopy
from pathlib import Path
from typing import Any
from crypto_wyckoff.io import DATA_DIR, ensure_dirs
ROLE_LOW = "1d"
ROLE_MID = "1w"
ROLE_HIGH = "1M"
# Minutes for ordering / validation (provider labels)
_TF_MINUTES: dict[str, int] = {
"1m": 1, "2m": 2, "3m": 3, "4m": 4, "5m": 5,
"10m": 10, "15m": 15, "20m": 20, "25m": 25, "30m": 30, "45m": 45,
"1h": 60, "2h": 120, "3h": 180, "4h": 240, "5h": 300,
"6h": 360, "7h": 420, "8h": 480, "9h": 540, "10h": 600,
"11h": 660, "12h": 720, "16h": 960, "20h": 1200,
"1d": 1440, "2d": 2880, "3d": 4320, "4d": 5760, "5d": 7200, "6d": 8640,
"1w": 10080, "2w": 20160, "3w": 30240,
"1M": 43200,
}
# TFs we allow in custom combos (provider-backed + local 1M)
ALLOWED_TFS: tuple[str, ...] = (
"1h", "2h", "3h", "4h", "6h", "8h", "12h",
"1d", "2d", "3d", "1w", "1M",
)
BUILTIN: list[dict[str, Any]] = [
{
"id": "h8_4_1",
"label": "8h / 4h / 1h",
"high": "8h",
"mid": "4h",
"low": "1h",
"builtin": True,
},
{
"id": "d_w_m",
"label": "1d / 1w / 1M",
"high": "1M",
"mid": "1w",
"low": "1d",
"builtin": True,
},
]
_COMBOS_FILE = DATA_DIR / "combos.json"
_lock = threading.Lock()
_cache: list[dict[str, Any]] | None = None
def tf_minutes(tf: str) -> int | None:
if tf in _TF_MINUTES:
return _TF_MINUTES[tf]
# tolerate provider typo "10" → skip
m = re.fullmatch(r"(\d+)([mhdwM])", tf)
if not m:
return None
n, u = int(m.group(1)), m.group(2)
mult = {"m": 1, "h": 60, "d": 1440, "w": 10080, "M": 43200}[u]
return n * mult
def combo_id_for(high: str, mid: str, low: str) -> str:
def _tok(t: str) -> str:
return t.replace("/", "_")
return f"{_tok(high)}_{_tok(mid)}_{_tok(low)}"
def validate_combo(high: str, mid: str, low: str) -> str | None:
"""Return error message or None if ok."""
for tf in (high, mid, low):
if tf not in ALLOWED_TFS:
return f"不支持的周期: {tf}"
if len({high, mid, low}) < 3:
return "高/中/低周期必须互不相同"
hm, mm, lm = tf_minutes(high), tf_minutes(mid), tf_minutes(low)
if hm is None or mm is None or lm is None:
return "无法解析周期长度"
if not (hm > mm > lm):
return "须满足 高 > 中 > 低(例如 8h > 4h > 1h"
return None
def _normalize(row: dict[str, Any]) -> dict[str, Any] | None:
high, mid, low = row.get("high"), row.get("mid"), row.get("low")
if not high or not mid or not low:
return None
err = validate_combo(str(high), str(mid), str(low))
if err:
return None
cid = str(row.get("id") or combo_id_for(high, mid, low))
label = str(row.get("label") or f"{high} / {mid} / {low}")
return {
"id": cid,
"label": label,
"high": str(high),
"mid": str(mid),
"low": str(low),
"builtin": bool(row.get("builtin", False)),
}
def _load_raw() -> list[dict[str, Any]]:
ensure_dirs()
if not _COMBOS_FILE.exists():
return deepcopy(BUILTIN)
try:
data = json.loads(_COMBOS_FILE.read_text(encoding="utf-8"))
items = data.get("combos") if isinstance(data, dict) else data
if not isinstance(items, list):
return deepcopy(BUILTIN)
except (OSError, json.JSONDecodeError):
return deepcopy(BUILTIN)
out: list[dict[str, Any]] = []
seen: set[str] = set()
for b in BUILTIN:
out.append(deepcopy(b))
seen.add(b["id"])
for row in items:
if not isinstance(row, dict):
continue
norm = _normalize(row)
if not norm or norm["id"] in seen:
continue
if norm["id"] in {b["id"] for b in BUILTIN}:
continue
norm["builtin"] = False
out.append(norm)
seen.add(norm["id"])
return out
def _save(combos: list[dict[str, Any]]) -> None:
ensure_dirs()
custom = [c for c in combos if not c.get("builtin")]
payload = {"combos": custom}
tmp = _COMBOS_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(_COMBOS_FILE)
def list_combos() -> list[dict[str, Any]]:
global _cache
with _lock:
if _cache is None:
_cache = _load_raw()
return deepcopy(_cache)
def get_combo(combo_id: str | None) -> dict[str, Any]:
combos = list_combos()
if combo_id:
for c in combos:
if c["id"] == combo_id:
return deepcopy(c)
return deepcopy(combos[0])
def add_combo(high: str, mid: str, low: str, label: str | None = None) -> dict[str, Any]:
err = validate_combo(high, mid, low)
if err:
raise ValueError(err)
cid = combo_id_for(high, mid, low)
row = {
"id": cid,
"label": label or f"{high} / {mid} / {low}",
"high": high,
"mid": mid,
"low": low,
"builtin": False,
}
with _lock:
combos = _load_raw()
for c in combos:
if c["id"] == cid or (c["high"], c["mid"], c["low"]) == (high, mid, low):
_cache = combos
return deepcopy(c)
combos.append(row)
_save(combos)
_cache = combos
return deepcopy(row)
def delete_combo(combo_id: str) -> bool:
with _lock:
combos = _load_raw()
kept: list[dict[str, Any]] = []
removed = False
for c in combos:
if c["id"] == combo_id:
if c.get("builtin"):
raise ValueError("内置组合不可删除")
removed = True
continue
kept.append(c)
if removed:
_save(kept)
_cache = kept
return removed
def all_tfs_for_combos(combos: list[dict[str, Any]] | None = None) -> list[str]:
"""Unique TFs needed by active combos (stable order)."""
rows = combos if combos is not None else list_combos()
seen: list[str] = []
for c in rows:
for k in ("low", "mid", "high"):
tf = c[k]
if tf not in seen:
seen.append(tf)
return seen
def lookback_for(tf: str) -> int:
defaults = {
"1h": 500,
"2h": 400,
"3h": 350,
"4h": 300,
"6h": 280,
"8h": 250,
"12h": 220,
"1d": 250,
"2d": 200,
"3d": 180,
"1w": 104,
"1M": 60,
}
return defaults.get(tf, 200)
-102
View File
@@ -1,102 +0,0 @@
"""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,
},
)
-195
View File
@@ -1,195 +0,0 @@
"""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},
},
},
)
-154
View File
@@ -1,154 +0,0 @@
"""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"
combo_id: str = "d_w_m"
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)
-149
View File
@@ -1,149 +0,0 @@
"""Event Engine — active concurrent events via Rule Registry.
Note: `active_events` are rules that fire on the latest bar snapshot,
NOT a historical SCARST 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,
},
)
-206
View File
@@ -1,206 +0,0 @@
"""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,
)
-363
View File
@@ -1,363 +0,0 @@
"""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 many TFs; 1M is resampled locally from daily UTC months.
LOOKBACK = {
"1h": 500,
"2h": 400,
"4h": 300,
"6h": 280,
"8h": 250,
"12h": 220,
"1d": 250,
"1w": 104,
"1M": 60,
}
# Default D/W/M stack (kept for compat); combos may request more TFs from provider.
TF_PROVIDER = ("1h", "4h", "8h", "1d", "1w")
TF_LIST = ("1d", "1w", "1M")
LOCAL_ONLY_TFS = frozenset({"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 is_intraday_tf(tf: str) -> bool:
"""True for minute/hour TFs that need clock time on charts."""
t = (tf or "").strip()
return t.endswith("m") or t.endswith("h")
def load_bars_with_ts(
symbol: str, tf: str, lookback: int | None = None
) -> list[dict]:
"""Return OHLCV rows with UTC ms ts (for chart labels).
``datetime`` is wall-clock in Asia/Shanghai (UTC+8) for display.
"""
from zoneinfo import ZoneInfo
tz_cn = ZoneInfo("Asia/Shanghai")
if lookback is None:
try:
from crypto_wyckoff.combos import lookback_for
lookback = lookback_for(tf)
except Exception:
lookback = LOOKBACK.get(tf, 100)
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()
out = []
for ts, o, h, l, c, v in rows:
dt_utc = datetime.fromtimestamp(ts / 1000.0, tz=timezone.utc)
dt_cn = dt_utc.astimezone(tz_cn)
out.append(
{
"ts": int(ts),
"datetime": dt_cn.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
"date": dt_cn.strftime("%Y-%m-%d"),
"open": o,
"high": h,
"low": l,
"close": c,
"volume": v,
}
)
return out
def load_frame(symbol: str, tf: str, lookback: int | None = None) -> OHLCVFrame | None:
rows = load_bars_with_ts(symbol, tf, lookback)
if not rows:
return None
return OHLCVFrame(
ts_code=symbol,
timeframe=tf,
trade_dates=[
datetime.fromtimestamp(r["ts"] / 1000.0, tz=timezone.utc).date() for r in rows
],
open=[r["open"] for r in rows],
high=[r["high"] for r in rows],
low=[r["low"] for r in rows],
close=[r["close"] for r in rows],
volume=[r["volume"] 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 requested TFs; monthly derived from daily when needed."""
wanted = list(dict.fromkeys(tfs))
stats: dict = {}
need_monthly = "1M" in wanted
if need_monthly and "1d" not in wanted:
wanted = ["1d", *wanted]
for tf in wanted:
if tf in LOCAL_ONLY_TFS:
continue
need = LOOKBACK.get(tf, 100)
if tf == "1d" and need_monthly:
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 need_monthly:
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."""
wanted = list(dict.fromkeys(tfs))
changed = False
for tf in wanted:
if tf in LOCAL_ONLY_TFS:
continue
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)
if "1M" in wanted:
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 []
-78
View File
@@ -1,78 +0,0 @@
"""Phase Engine — Phase AE via Rule Registry."""
from __future__ import annotations
from crypto_wyckoff.domain_models import EngineResult, WyckoffPhase
from crypto_wyckoff.rules.registry import rule_registry
class PhaseEngine:
name = "Phase"
version = "1.0.0"
def run(self, cycle: EngineResult, feature: EngineResult, timeframe: str) -> EngineResult:
if feature.payload.get("insufficient") or cycle.payload.get("cycle") == "Unknown":
return EngineResult(
name=self.name,
version=self.version,
confidence=20.0,
score=30.0,
reasons=["数据/周期不足,Phase=None"],
warnings=["insufficient_features"],
payload={
"phase": WyckoffPhase.NONE.value,
"timeframe": timeframe,
"cycle": cycle.payload.get("cycle"),
"structure_score": 30.0,
},
)
context = {
"features": feature.payload,
"cycle": cycle.payload,
"timeframe": timeframe,
}
hits = []
for rule in rule_registry.by_category("phase", timeframe):
hit = rule.evaluate(context)
if hit and hit.phase:
hits.append(hit)
if not hits:
return EngineResult(
name=self.name,
version=self.version,
confidence=40.0,
score=cycle.score * 0.5,
reasons=["未识别明确 Phase"],
payload={
"phase": WyckoffPhase.NONE.value,
"timeframe": timeframe,
"cycle": cycle.payload.get("cycle"),
"structure_score": cycle.score * 0.5,
},
)
best = max(hits, key=lambda h: h.confidence)
structure_score = best.score
# Phase D/E stronger structure
if best.phase in (WyckoffPhase.D.value, WyckoffPhase.E.value):
structure_score = max(structure_score, 80.0)
elif best.phase == WyckoffPhase.C.value:
structure_score = max(structure_score, 72.0)
return EngineResult(
name=self.name,
version=self.version,
confidence=best.confidence,
score=structure_score,
reasons=best.reasons,
metrics=best.metrics,
payload={
"phase": best.phase,
"timeframe": timeframe,
"cycle": cycle.payload.get("cycle"),
"rule_id": best.rule_id,
"structure_score": structure_score,
},
)
-181
View File
@@ -1,181 +0,0 @@
"""Scan pipeline: load local frames → engines → store (per TF combo)."""
from __future__ import annotations
import json
import logging
from datetime import date, datetime, timezone
from crypto_wyckoff.combos import ROLE_HIGH, ROLE_LOW, ROLE_MID, get_combo, lookback_for
from crypto_wyckoff.cycle import CycleEngine
from crypto_wyckoff.decision import DecisionEngine
from crypto_wyckoff.domain_models import WyckoffScanRow
from crypto_wyckoff.event import EventEngine
from crypto_wyckoff.features import FeatureEngine
from crypto_wyckoff.io import load_frame
from crypto_wyckoff.phase import PhaseEngine
from crypto_wyckoff.plan import PlanEngine
from crypto_wyckoff.signal import SignalEngine
from crypto_wyckoff.store import upsert_row
from crypto_wyckoff.symbols_cn import display_name_cn
from crypto_wyckoff.version import WYCKOFF_ENGINE_VERSION
logger = logging.getLogger(__name__)
def analyze_symbol(
low_frame,
mid_frame,
high_frame,
*,
feature_eng: FeatureEngine,
cycle_eng: CycleEngine,
phase_eng: PhaseEngine,
event_eng: EventEngine,
signal_eng: SignalEngine,
decision_eng: DecisionEngine,
plan_eng: PlanEngine,
) -> dict:
"""Run engines with D/W/M *role* aliases so existing rules match.
Frames may be any TF combo (e.g. 1h/4h/8h); rules still see 1d/1w/1M roles.
"""
f_d = feature_eng.run(low_frame, ROLE_LOW)
f_w = feature_eng.run(mid_frame, ROLE_MID)
f_m = feature_eng.run(high_frame, ROLE_HIGH)
c_m = cycle_eng.run(f_m, ROLE_HIGH)
c_w = cycle_eng.run(f_w, ROLE_MID)
p_w = phase_eng.run(c_w, f_w, ROLE_MID)
p_d = phase_eng.run(c_w, f_d, ROLE_LOW)
e_w = event_eng.run(c_w, p_w, f_w, ROLE_MID)
e_d = event_eng.run(c_w, p_d, f_d, ROLE_LOW)
s_d = signal_eng.run(e_d, p_d)
decision = decision_eng.run(c_m, c_w, p_w, e_w, e_d, s_d)
plan = plan_eng.run(f_d, decision)
return {
"f_d": f_d, "f_w": f_w, "f_m": f_m,
"c_m": c_m, "c_w": c_w, "p_w": p_w,
"e_w": e_w, "e_d": e_d, "s_d": s_d,
"decision": decision, "plan": plan,
}
def _to_row(
trade_date: date,
symbol: str,
result: dict,
*,
combo_id: str,
combo_label: str,
) -> WyckoffScanRow:
d = result["decision"]
p = result["plan"]
c_m, c_w, p_w = result["c_m"], result["c_w"], result["p_w"]
e_w, e_d, s_d = result["e_w"], result["e_d"], result["s_d"]
f_d, f_w, f_m = result["f_d"], result["f_w"], result["f_m"]
snapshot = {
"combo_id": combo_id,
"combo_label": combo_label,
"daily": {k: f_d.payload.get(k) for k in (
"ma20", "ma60", "ma120", "atr", "adx", "volume_ratio",
"range_high", "range_low", "swing_high", "swing_low", "close",
)},
"weekly": {k: f_w.payload.get(k) for k in ("ma20", "ma60", "adx", "close")},
"monthly": {k: f_m.payload.get(k) for k in ("ma20", "ma60", "adx", "close")},
}
markers = []
for key, typ in (("entry", "entry"), ("stop", "stop"), ("target1", "target1"), ("target2", "target2")):
if p.payload.get(key) is not None:
markers.append({"type": typ, "price": p.payload[key]})
return WyckoffScanRow(
trade_date=trade_date,
ts_code=symbol,
name=display_name_cn(symbol),
industry="crypto",
engine_version=WYCKOFF_ENGINE_VERSION,
m_cycle=c_m.payload.get("cycle", "Unknown"),
cycle_confidence=c_m.confidence,
trend_score=float(d.payload.get("trend_score", c_m.score)),
w_cycle=c_w.payload.get("cycle", "Unknown"),
w_phase=p_w.payload.get("phase", "None"),
w_current_event=e_w.payload.get("current_event", "None"),
w_recent_events_json=json.dumps(
e_w.payload.get("active_events") or e_w.payload.get("recent_events") or [],
ensure_ascii=False,
),
phase_confidence=p_w.confidence,
structure_score=float(d.payload.get("structure_score", p_w.score)),
d_current_event=e_d.payload.get("current_event", "None"),
d_recent_events_json=json.dumps(
e_d.payload.get("active_events") or e_d.payload.get("recent_events") or [],
ensure_ascii=False,
),
event_confidence=e_d.confidence,
entry_score=float(d.payload.get("entry_score", e_d.score)),
entry=p.payload.get("entry"),
stop=p.payload.get("stop"),
target1=p.payload.get("target1"),
target2=p.payload.get("target2"),
rr=p.payload.get("rr"),
alignment=float(d.payload.get("alignment", 0)),
stars=int(d.payload.get("stars", 1)),
decision_signal=d.payload.get("decision_signal", "Watch"),
signal_confidence=s_d.confidence,
overall_confidence=float(d.payload.get("overall_confidence", d.confidence)),
overall_score=float(d.payload.get("overall_score", d.score)),
risk=d.payload.get("risk", "Medium"),
reasons_json=json.dumps(d.reasons + d.warnings, ensure_ascii=False),
feature_snapshot_json=json.dumps(snapshot, ensure_ascii=False),
markers_json=json.dumps(markers, ensure_ascii=False),
scanned_at=datetime.now(timezone.utc),
combo_id=combo_id,
)
_ENGINES = None
def _engines():
global _ENGINES
if _ENGINES is None:
_ENGINES = {
"feature_eng": FeatureEngine(),
"cycle_eng": CycleEngine(),
"phase_eng": PhaseEngine(),
"event_eng": EventEngine(),
"signal_eng": SignalEngine(),
"decision_eng": DecisionEngine(),
"plan_eng": PlanEngine(),
}
return _ENGINES
def analyze_and_store(
symbol: str,
trade_date: date | None = None,
*,
combo_id: str | None = None,
) -> WyckoffScanRow | None:
eng = _engines()
combo = get_combo(combo_id)
low_tf, mid_tf, high_tf = combo["low"], combo["mid"], combo["high"]
low = load_frame(symbol, low_tf, lookback_for(low_tf))
mid = load_frame(symbol, mid_tf, lookback_for(mid_tf))
high = load_frame(symbol, high_tf, lookback_for(high_tf))
if low is None or len(low) < 40:
return None
result = analyze_symbol(low, mid, high, **eng)
td = trade_date or (
low.trade_dates[-1] if low.trade_dates else datetime.now(timezone.utc).date()
)
row = _to_row(td, symbol, result, combo_id=combo["id"], combo_label=combo["label"])
upsert_row(row)
return row
-78
View File
@@ -1,78 +0,0 @@
"""Plan Engine — Entry / Stop / Target / RR only when Decision is tradable."""
from __future__ import annotations
from crypto_wyckoff.domain_models import DecisionSignal, EngineResult
_TRADABLE = {
DecisionSignal.STRONG_BUY.value,
DecisionSignal.BUY.value,
DecisionSignal.SELL.value,
}
class PlanEngine:
name = "Plan"
version = "1.0.0"
def run(self, daily_feature: EngineResult, decision: EngineResult) -> EngineResult:
f = daily_feature.payload
close = float(f.get("close") or 0)
atr = float(f.get("atr") or 0) or close * 0.02
swing_low = float(f.get("swing_low") or close - 2 * atr)
swing_high = float(f.get("swing_high") or close + 2 * atr)
range_high = float(f.get("range_high") or swing_high)
signal = decision.payload.get("decision_signal", DecisionSignal.WATCH.value)
entry = stop = t1 = t2 = rr = None
reasons: list[str] = []
if signal not in _TRADABLE or close <= 0:
reasons.append(f"无交易计划(信号={signal}")
return EngineResult(
name=self.name,
version=self.version,
confidence=decision.confidence,
score=decision.score,
reasons=reasons,
payload={
"entry": None,
"stop": None,
"target1": None,
"target2": None,
"rr": None,
},
)
if signal in (DecisionSignal.STRONG_BUY.value, DecisionSignal.BUY.value):
entry = round(close, 4)
stop = round(min(swing_low, close - 1.5 * atr), 4)
risk = max(entry - stop, 1e-6)
t1 = round(entry + 2.0 * risk, 4)
t2 = round(max(range_high, entry + 3.0 * risk), 4)
rr = round((t1 - entry) / risk, 2)
reasons.append(f"入场={entry} 止损={stop} 目标一={t1} 盈亏比={rr}")
else: # Sell
entry = round(close, 4)
stop = round(max(swing_high, close + 1.5 * atr), 4)
risk = max(stop - entry, 1e-6)
t1 = round(entry - 2.0 * risk, 4)
t2 = round(entry - 3.0 * risk, 4)
rr = round((entry - t1) / risk, 2)
reasons.append(f"做空计划 入场={entry} 止损={stop} 目标一={t1}")
return EngineResult(
name=self.name,
version=self.version,
confidence=decision.confidence,
score=decision.score,
reasons=reasons,
payload={
"entry": entry,
"stop": stop,
"target1": t1,
"target2": t2,
"rr": rr,
},
)
-3
View File
@@ -1,3 +0,0 @@
from crypto_wyckoff.rules.registry import rule_registry
__all__ = ["rule_registry"]
-33
View File
@@ -1,33 +0,0 @@
"""Rule protocol for Wyckoff Rule Registry."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
@dataclass
class RuleHit:
"""A single rule match."""
rule_id: str
event: str | None = None
phase: str | None = None
cycle: str | None = None
confidence: float = 0.0
score: float = 0.0
reasons: list[str] = field(default_factory=list)
metrics: dict[str, Any] = field(default_factory=dict)
class WyckoffRule(ABC):
"""Pluggable rule. Engines iterate registry; never hardcode rule lists."""
rule_id: str
category: str # cycle | phase | event
timeframes: tuple[str, ...] = ("1d", "1w", "1M")
@abstractmethod
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
"""Return RuleHit if matched, else None. Pure — no I/O."""
-126
View File
@@ -1,126 +0,0 @@
"""Cycle classification rules (monthly / weekly)."""
from __future__ import annotations
from typing import Any
from crypto_wyckoff.domain_models import WyckoffCycle
from crypto_wyckoff.rules.base import RuleHit, WyckoffRule
def _f(ctx: dict[str, Any], key: str, default: float = 0.0) -> float:
v = ctx.get("features", {}).get(key, default)
try:
return float(v) if v is not None else default
except (TypeError, ValueError):
return default
class MarkupCycleRule(WyckoffRule):
rule_id = "cycle_markup"
category = "cycle"
timeframes = ("1M", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
close = _f(context, "close")
ma20 = _f(context, "ma20")
ma60 = _f(context, "ma60")
ma120 = _f(context, "ma120")
adx = _f(context, "adx")
slope = _f(context, "ma60_slope")
if close > ma20 > ma60 and (ma60 >= ma120 or slope > 0) and adx >= 18:
conf = min(95.0, 55 + adx + (10 if close > ma120 else 0))
return RuleHit(
rule_id=self.rule_id,
cycle=WyckoffCycle.MARKUP.value,
confidence=conf,
score=conf,
reasons=["价格位于均线多头排列", f"ADX={adx:.1f}"],
metrics={"adx": adx, "slope": slope},
)
return None
class MarkdownCycleRule(WyckoffRule):
rule_id = "cycle_markdown"
category = "cycle"
timeframes = ("1M", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
close = _f(context, "close")
ma20 = _f(context, "ma20")
ma60 = _f(context, "ma60")
ma120 = _f(context, "ma120")
adx = _f(context, "adx")
slope = _f(context, "ma60_slope")
if close < ma20 < ma60 and (ma60 <= ma120 or slope < 0) and adx >= 18:
conf = min(95.0, 55 + adx + (10 if close < ma120 else 0))
return RuleHit(
rule_id=self.rule_id,
cycle=WyckoffCycle.MARKDOWN.value,
confidence=conf,
score=conf,
reasons=["价格位于均线空头排列", f"ADX={adx:.1f}"],
metrics={"adx": adx},
)
return None
class AccumulationCycleRule(WyckoffRule):
rule_id = "cycle_accumulation"
category = "cycle"
timeframes = ("1M", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
adx = _f(context, "adx")
range_pct = _f(context, "range_pct_60")
close = _f(context, "close")
ma120 = _f(context, "ma120")
vol_trend = _f(context, "volume_trend")
# Range-bound after decline: strictly at/below MA120 (mutually exclusive vs Distribution)
if adx < 22 and range_pct < 0.28 and close <= ma120:
conf = 60 + (10 if vol_trend > 0 else 0) + (10 if close < ma120 else 0)
return RuleHit(
rule_id=self.rule_id,
cycle=WyckoffCycle.ACCUMULATION.value,
confidence=min(90.0, conf),
score=min(90.0, conf),
reasons=["低趋势强度区间震荡", "疑似吸筹区间"],
metrics={"adx": adx, "range_pct_60": range_pct},
)
return None
class DistributionCycleRule(WyckoffRule):
rule_id = "cycle_distribution"
category = "cycle"
timeframes = ("1M", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
adx = _f(context, "adx")
range_pct = _f(context, "range_pct_60")
close = _f(context, "close")
ma120 = _f(context, "ma120")
vol_trend = _f(context, "volume_trend")
# Range-bound near highs: strictly above MA120 (mutually exclusive vs Accumulation)
if adx < 22 and range_pct < 0.28 and close > ma120:
conf = 60 + (10 if vol_trend < 0 else 0) + (10 if close > ma120 else 0)
return RuleHit(
rule_id=self.rule_id,
cycle=WyckoffCycle.DISTRIBUTION.value,
confidence=min(90.0, conf),
score=min(90.0, conf),
reasons=["高位低趋势震荡", "疑似派发区间"],
metrics={"adx": adx, "range_pct_60": range_pct},
)
return None
def build_rules() -> list[WyckoffRule]:
# Order: trend cycles first (more decisive), then range cycles
return [
MarkupCycleRule(),
MarkdownCycleRule(),
AccumulationCycleRule(),
DistributionCycleRule(),
]
-254
View File
@@ -1,254 +0,0 @@
"""Event rules: Spring/SOS/LPS/UTAD/SC/AR/ST/..."""
from __future__ import annotations
from typing import Any
from crypto_wyckoff.domain_models import WyckoffCycle, WyckoffEvent, WyckoffPhase
from crypto_wyckoff.rules.base import RuleHit, WyckoffRule
def _f(ctx: dict[str, Any], key: str, default: float = 0.0) -> float:
v = ctx.get("features", {}).get(key, default)
try:
return float(v) if v is not None else default
except (TypeError, ValueError):
return default
def _cycle(ctx: dict[str, Any]) -> str:
return (ctx.get("cycle") or {}).get("cycle") or ""
def _phase(ctx: dict[str, Any]) -> str:
return (ctx.get("phase") or {}).get("phase") or ""
class SpringRule(WyckoffRule):
rule_id = "event_spring"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
if cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.RE_ACCUMULATION.value,
WyckoffCycle.MARKUP.value):
# Allow spring only in accumulative contexts; Decision will filter MTF
if cycle == WyckoffCycle.DISTRIBUTION.value:
pass # still detect for facts but lower confidence
pierce = _f(context, "pierce_below_range")
reclaim = _f(context, "reclaim_speed")
vol_ratio = _f(context, "volume_ratio")
close_in_range = _f(context, "close_back_in_range")
if pierce >= 0.002 and close_in_range >= 0.5 and reclaim >= 0.3:
strength = min(98.0, 50 + pierce * 2000 + reclaim * 20 + (15 if vol_ratio < 1.2 else 5))
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SPRING.value,
confidence=strength,
score=strength,
reasons=[
f"跌破区间后收回 (pierce={pierce:.3%})",
f"回收速度={reclaim:.2f}",
f"量比={vol_ratio:.2f}",
],
metrics={"pierce": pierce, "reclaim": reclaim, "volume_ratio": vol_ratio},
)
return None
class TestRule(WyckoffRule):
rule_id = "event_test"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pos = _f(context, "range_position")
vol_ratio = _f(context, "volume_ratio")
near_low = pos < 0.2
if near_low and vol_ratio < 0.85:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.TEST.value,
confidence=68.0,
score=65.0,
reasons=["低位缩量回测"],
)
return None
class SOSRule(WyckoffRule):
rule_id = "event_sos"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
breakout = _f(context, "breakout_above_range")
vol_ratio = _f(context, "volume_ratio")
close = _f(context, "close")
ma20 = _f(context, "ma20")
if breakout >= 0.0 and vol_ratio >= 1.2 and close > ma20:
conf = min(95.0, 70 + vol_ratio * 8)
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SOS.value,
confidence=conf,
score=conf,
reasons=["放量突破区间上沿 (SOS)"],
metrics={"vol_ratio": vol_ratio},
)
return None
class LPSRule(WyckoffRule):
rule_id = "event_lps"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
# Pullback hold above broken range / MA20 after prior strength
pullback = _f(context, "pullback_hold")
vol_ratio = _f(context, "volume_ratio")
above_ma = _f(context, "close") > _f(context, "ma20")
if pullback >= 0.5 and above_ma and vol_ratio <= 1.1:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.LPS.value,
confidence=74.0,
score=76.0,
reasons=["突破后缩量回踩支撑 (LPS)"],
)
return None
class SCRule(WyckoffRule):
rule_id = "event_sc"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
vol_ratio = _f(context, "volume_ratio")
bar_range = _f(context, "bar_range_atr")
pos = _f(context, "range_position")
if vol_ratio >= 1.8 and bar_range >= 1.5 and pos < 0.35:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SC.value,
confidence=72.0,
score=70.0,
reasons=["低位放量宽幅,疑似 Selling Climax"],
)
return None
class ARRule(WyckoffRule):
rule_id = "event_ar"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
# Automatic rally: bounce from lows
bounce = _f(context, "bounce_from_low")
if bounce >= 0.04:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.AR.value,
confidence=65.0,
score=62.0,
reasons=["低点后自动反弹 (AR)"],
)
return None
class STRule(WyckoffRule):
rule_id = "event_st"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pos = _f(context, "range_position")
vol_ratio = _f(context, "volume_ratio")
if 0.15 < pos < 0.45 and vol_ratio < 1.0:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.ST.value,
confidence=60.0,
score=58.0,
reasons=["次级测试 (ST)"],
)
return None
class UTADRule(WyckoffRule):
rule_id = "event_utad"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
pierce_up = _f(context, "pierce_above_range")
fail = _f(context, "fail_back_into_range")
if cycle in (WyckoffCycle.DISTRIBUTION.value, WyckoffCycle.RE_DISTRIBUTION.value,
WyckoffCycle.MARKUP.value):
if pierce_up >= 0.002 and fail >= 0.5:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.UTAD.value,
confidence=76.0,
score=74.0,
reasons=["冲高失败回到区间 (UTAD)"],
)
return None
class JumpRule(WyckoffRule):
rule_id = "event_jump"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
gap = _f(context, "gap_up_pct")
vol_ratio = _f(context, "volume_ratio")
if gap >= 0.03 and vol_ratio >= 1.3:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.JUMP.value,
confidence=70.0,
score=72.0,
reasons=["放量向上跳跃 (Jump)"],
)
return None
class BackupRule(WyckoffRule):
rule_id = "event_backup"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pullback = _f(context, "pullback_hold")
after_jump = _f(context, "after_strength")
if after_jump >= 0.5 and pullback >= 0.5:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.BACKUP.value,
confidence=68.0,
score=70.0,
reasons=["跳跃后回踩 (Backup)"],
)
return None
def build_rules() -> list[WyckoffRule]:
return [
SpringRule(),
UTADRule(),
SOSRule(),
LPSRule(),
SCRule(),
JumpRule(),
BackupRule(),
TestRule(),
ARRule(),
STRule(),
]
-163
View File
@@ -1,163 +0,0 @@
"""Phase AE rules (primarily weekly)."""
from __future__ import annotations
from typing import Any
from crypto_wyckoff.domain_models import WyckoffCycle, WyckoffPhase
from crypto_wyckoff.rules.base import RuleHit, WyckoffRule
def _f(ctx: dict[str, Any], key: str, default: float = 0.0) -> float:
v = ctx.get("features", {}).get(key, default)
try:
return float(v) if v is not None else default
except (TypeError, ValueError):
return default
def _cycle(ctx: dict[str, Any]) -> str:
return (ctx.get("cycle") or {}).get("cycle") or WyckoffCycle.UNKNOWN.value
class PhaseARule(WyckoffRule):
rule_id = "phase_a"
category = "phase"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
if cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.DISTRIBUTION.value,
WyckoffCycle.RE_ACCUMULATION.value, WyckoffCycle.RE_DISTRIBUTION.value):
return None
# Stopping action: high vol + large range recently, still range-bound
vol_ratio = _f(context, "volume_ratio")
range_last = _f(context, "bar_range_atr")
if vol_ratio >= 1.4 and range_last >= 1.2:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.A.value,
confidence=70.0,
score=65.0,
reasons=["放量宽幅波动,疑似 Phase A 停止行为"],
)
return None
class PhaseBRule(WyckoffRule):
rule_id = "phase_b"
category = "phase"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
if cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.DISTRIBUTION.value):
return None
adx = _f(context, "adx")
range_pct = _f(context, "range_pct_60")
pos = _f(context, "range_position") # 0=low 1=high of range
if adx < 20 and 0.25 < pos < 0.75 and range_pct < 0.30:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.B.value,
confidence=72.0,
score=68.0,
reasons=["区间中部震荡,疑似 Phase B 建仓/派发"],
)
return None
class PhaseCRule(WyckoffRule):
rule_id = "phase_c"
category = "phase"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
pos = _f(context, "range_position")
spring_like = _f(context, "spring_score_hint")
utad_like = _f(context, "utad_score_hint")
if cycle in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.RE_ACCUMULATION.value):
if pos < 0.25 or spring_like >= 50:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.C.value,
confidence=75.0 + min(15.0, spring_like * 0.15),
score=78.0,
reasons=["区间低位测试,疑似 Phase C (Spring/Test)"],
)
if cycle in (WyckoffCycle.DISTRIBUTION.value, WyckoffCycle.RE_DISTRIBUTION.value):
if pos > 0.75 or utad_like >= 50:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.C.value,
confidence=75.0,
score=78.0,
reasons=["区间高位测试,疑似 Phase C (UTAD)"],
)
return None
class PhaseDRule(WyckoffRule):
rule_id = "phase_d"
category = "phase"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
close = _f(context, "close")
ma20 = _f(context, "ma20")
range_high = _f(context, "range_high")
range_low = _f(context, "range_low")
vol_ratio = _f(context, "volume_ratio")
if cycle in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.RE_ACCUMULATION.value):
if close > ma20 and range_high > 0 and close >= range_high * 0.98 and vol_ratio >= 1.1:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.D.value,
confidence=80.0,
score=82.0,
reasons=["突破区间上沿放量,疑似 Phase D SOS"],
)
if cycle in (WyckoffCycle.DISTRIBUTION.value, WyckoffCycle.RE_DISTRIBUTION.value):
if close < ma20 and range_low > 0 and close <= range_low * 1.02:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.D.value,
confidence=80.0,
score=82.0,
reasons=["跌破区间下沿,疑似 Phase D SOW"],
)
return None
class PhaseERule(WyckoffRule):
rule_id = "phase_e"
category = "phase"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
# Markup/Markdown already imply trend continuation (Phase E of prior structure)
if cycle == WyckoffCycle.MARKUP.value:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.E.value,
confidence=78.0,
score=80.0,
reasons=["趋势上行,对应 Phase E Markup"],
)
if cycle == WyckoffCycle.MARKDOWN.value:
return RuleHit(
rule_id=self.rule_id,
phase=WyckoffPhase.E.value,
confidence=78.0,
score=80.0,
reasons=["趋势下行,对应 Phase E Markdown"],
)
return None
def build_rules() -> list[WyckoffRule]:
# More specific phases first
return [PhaseDRule(), PhaseCRule(), PhaseARule(), PhaseBRule(), PhaseERule()]
-39
View File
@@ -1,39 +0,0 @@
"""Rule Registry — register Wyckoff rules without modifying engines."""
from __future__ import annotations
from crypto_wyckoff.rules.base import WyckoffRule
class RuleRegistry:
def __init__(self) -> None:
self._rules: dict[str, WyckoffRule] = {}
def register(self, rule: WyckoffRule) -> None:
self._rules[rule.rule_id] = rule
def get(self, rule_id: str) -> WyckoffRule | None:
return self._rules.get(rule_id)
def by_category(self, category: str, timeframe: str | None = None) -> list[WyckoffRule]:
out = [r for r in self._rules.values() if r.category == category]
if timeframe:
out = [r for r in out if timeframe in r.timeframes]
return out
def all(self) -> list[WyckoffRule]:
return list(self._rules.values())
rule_registry = RuleRegistry()
def _register_defaults() -> None:
from crypto_wyckoff.rules import cycle_rules, event_rules, phase_rules
for mod in (cycle_rules, phase_rules, event_rules):
for rule in mod.build_rules():
rule_registry.register(rule)
_register_defaults()
-127
View File
@@ -1,127 +0,0 @@
"""Background tip + scan scheduler for crypto wyckoff (all enabled combos)."""
from __future__ import annotations
import logging
import threading
from datetime import datetime, timezone
from crypto_wyckoff.combos import all_tfs_for_combos, list_combos
from crypto_wyckoff.io import (
backfill_symbol,
bar_count,
fetch_symbols_from_provider,
tip_update_symbol,
)
from crypto_wyckoff.pipeline import analyze_and_store
logger = logging.getLogger(__name__)
_thread: threading.Thread | None = None
_stop = threading.Event()
_status: dict = {
"running": False,
"last_tick_at": None,
"last_error": None,
"symbols_total": 0,
"symbols_scanned": 0,
"tick_interval_sec": 60,
"backfill_done": False,
}
_status_lock = threading.Lock()
def _set(**kwargs):
with _status_lock:
_status.update(kwargs)
def get_status() -> dict:
with _status_lock:
return dict(_status)
def run_tick(max_symbols: int | None = None, force_rescan: bool = False) -> dict:
"""One cycle: refresh symbols, tip-update, analyze each combo."""
symbols = fetch_symbols_from_provider()
if max_symbols:
symbols = symbols[:max_symbols]
combos = list_combos()
tfs = all_tfs_for_combos(combos)
_set(symbols_total=len(symbols), running=True, last_error=None)
scanned = 0
errors = 0
changed_n = 0
for i, sym in enumerate(symbols):
try:
# Prefer low-TF of first combo for "enough history" gate
low0 = combos[0]["low"] if combos else "1d"
if bar_count(sym, low0) < 40:
backfill_symbol(sym, tfs)
tip_changed = tip_update_symbol(sym, tfs)
if tip_changed:
changed_n += 1
if force_rescan or tip_changed:
for combo in combos:
row = analyze_and_store(sym, combo_id=combo["id"])
if row:
scanned += 1
except Exception as e:
errors += 1
if errors <= 5:
logger.warning("tick %s: %s", sym, e)
_set(last_error=str(e))
if (i + 1) % 25 == 0:
_set(symbols_scanned=scanned)
logger.info("wyckoff tick progress %s/%s scanned=%s", i + 1, len(symbols), scanned)
_set(
running=False,
symbols_scanned=scanned,
last_tick_at=datetime.now(timezone.utc).isoformat(),
backfill_done=True,
)
return {
"symbols": len(symbols),
"scanned": scanned,
"changed_tips": changed_n,
"errors": errors,
"combos": [c["id"] for c in combos],
"tfs": tfs,
}
def _loop(interval: int, max_symbols: int | None):
try:
run_tick(max_symbols=max_symbols, force_rescan=True)
except Exception as e:
logger.exception("initial tick failed: %s", e)
_set(last_error=str(e), running=False)
while not _stop.wait(interval):
try:
# Tip-driven: only force full rescan when tips change is handled inside
run_tick(max_symbols=max_symbols, force_rescan=False)
except Exception as e:
logger.exception("tick failed: %s", e)
_set(last_error=str(e), running=False)
def start_scheduler(interval_sec: int = 60, max_symbols: int | None = None) -> None:
global _thread
if _thread and _thread.is_alive():
return
_stop.clear()
_set(tick_interval_sec=interval_sec)
_thread = threading.Thread(
target=_loop,
args=(interval_sec, max_symbols),
name="crypto-wyckoff-scheduler",
daemon=True,
)
_thread.start()
logger.info("crypto wyckoff scheduler started interval=%ss", interval_sec)
def stop_scheduler() -> None:
_stop.set()
-35
View File
@@ -1,35 +0,0 @@
"""Signal Engine — timeframe-local status labels only (not tradability)."""
from __future__ import annotations
from crypto_wyckoff.domain_models import EngineResult, WyckoffEvent
class SignalEngine:
"""Maps local Event/Phase into a status label. Decision decides tradability."""
name = "Signal"
version = "1.0.0"
def run(self, event: EngineResult, phase: EngineResult | None = None) -> EngineResult:
current = event.payload.get("current_event", WyckoffEvent.NONE.value)
conf = event.confidence
label = current # status label mirrors event for V1
reasons = [f"本地事件标签: {label}"]
if phase and phase.payload.get("phase"):
reasons.append(f"本地阶段: {phase.payload.get('phase')}")
return EngineResult(
name=self.name,
version=self.version,
confidence=conf,
score=event.score,
reasons=reasons,
payload={
"signal_label": label,
"current_event": current,
"phase": (phase.payload.get("phase") if phase else None),
"active_events": event.payload.get("active_events")
or event.payload.get("recent_events", []),
},
)
-236
View File
@@ -1,236 +0,0 @@
"""SQLite persistence for crypto wyckoff scan rows (per combo)."""
from __future__ import annotations
import sqlite3
from datetime import datetime
from typing import Any
from crypto_wyckoff.domain_models import WyckoffScanRow
from crypto_wyckoff.io import SCAN_DB, ensure_dirs
_COLS = [
"trade_date", "combo_id", "ts_code", "name", "industry", "engine_version",
"m_cycle", "cycle_confidence", "trend_score",
"w_cycle", "w_phase", "w_current_event", "w_recent_events_json",
"phase_confidence", "structure_score",
"d_current_event", "d_recent_events_json", "event_confidence", "entry_score",
"entry", "stop", "target1", "target2", "rr",
"alignment", "stars", "decision_signal", "signal_confidence",
"overall_confidence", "overall_score", "risk", "reasons_json",
"feature_snapshot_json", "markers_json", "scanned_at",
]
_CREATE_SQL = """
CREATE TABLE IF NOT EXISTS wyckoff_scan (
trade_date TEXT NOT NULL,
combo_id TEXT NOT NULL DEFAULT 'd_w_m',
ts_code TEXT NOT NULL,
name TEXT DEFAULT '',
industry TEXT DEFAULT '',
engine_version TEXT,
m_cycle TEXT, cycle_confidence REAL, trend_score REAL,
w_cycle TEXT, w_phase TEXT, w_current_event TEXT, w_recent_events_json TEXT,
phase_confidence REAL, structure_score REAL,
d_current_event TEXT, d_recent_events_json TEXT, event_confidence REAL, entry_score REAL,
entry REAL, stop REAL, target1 REAL, target2 REAL, rr REAL,
alignment REAL, stars INTEGER, decision_signal TEXT, signal_confidence REAL,
overall_confidence REAL, overall_score REAL, risk TEXT, reasons_json TEXT,
feature_snapshot_json TEXT, markers_json TEXT, scanned_at TEXT,
PRIMARY KEY (trade_date, combo_id, ts_code)
)
"""
def _migrate(c: sqlite3.Connection) -> None:
cur = c.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='wyckoff_scan'"
)
if not cur.fetchone():
c.execute(_CREATE_SQL)
c.execute(
"CREATE INDEX IF NOT EXISTS idx_cw_score "
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
)
return
cols = {r[1] for r in c.execute("PRAGMA table_info(wyckoff_scan)")}
if "combo_id" in cols:
c.execute(
"CREATE INDEX IF NOT EXISTS idx_cw_score "
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
)
return
# Legacy PK (trade_date, ts_code) → add combo_id via table rebuild
c.execute("ALTER TABLE wyckoff_scan RENAME TO wyckoff_scan_old")
c.execute(_CREATE_SQL)
old_cols = [r[1] for r in c.execute("PRAGMA table_info(wyckoff_scan_old)")]
shared = [col for col in _COLS if col != "combo_id" and col in old_cols]
col_sql = ",".join(shared)
c.execute(
f"""
INSERT INTO wyckoff_scan (combo_id, {col_sql})
SELECT 'd_w_m', {col_sql} FROM wyckoff_scan_old
"""
)
c.execute("DROP TABLE wyckoff_scan_old")
c.execute(
"CREATE INDEX IF NOT EXISTS idx_cw_score "
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
)
def _conn() -> sqlite3.Connection:
ensure_dirs()
c = sqlite3.connect(str(SCAN_DB), timeout=60)
c.row_factory = sqlite3.Row
_migrate(c)
c.commit()
return c
def upsert_row(row: WyckoffScanRow) -> None:
combo_id = getattr(row, "combo_id", None) or "d_w_m"
vals = (
row.trade_date.isoformat() if hasattr(row.trade_date, "isoformat") else str(row.trade_date),
combo_id,
row.ts_code, row.name, row.industry, row.engine_version,
row.m_cycle, row.cycle_confidence, row.trend_score,
row.w_cycle, row.w_phase, row.w_current_event, row.w_recent_events_json,
row.phase_confidence, row.structure_score,
row.d_current_event, row.d_recent_events_json, row.event_confidence, row.entry_score,
row.entry, row.stop, row.target1, row.target2, row.rr,
row.alignment, row.stars, row.decision_signal, row.signal_confidence,
row.overall_confidence, row.overall_score, row.risk, row.reasons_json,
row.feature_snapshot_json, row.markers_json,
row.scanned_at.isoformat() if isinstance(row.scanned_at, datetime) else str(row.scanned_at),
)
c = _conn()
try:
placeholders = ",".join("?" * len(_COLS))
col_sql = ",".join(_COLS)
updates = ",".join(
f"{col}=excluded.{col}"
for col in _COLS
if col not in ("trade_date", "combo_id", "ts_code")
)
c.execute(
f"""
INSERT INTO wyckoff_scan ({col_sql}) VALUES ({placeholders})
ON CONFLICT(trade_date, combo_id, ts_code) DO UPDATE SET {updates}
""",
vals,
)
c.commit()
finally:
c.close()
def latest_trade_date(combo_id: str | None = None) -> str | None:
c = _conn()
try:
if combo_id:
cur = c.execute(
"SELECT MAX(trade_date) FROM wyckoff_scan WHERE combo_id=?",
(combo_id,),
)
else:
cur = c.execute("SELECT MAX(trade_date) FROM wyckoff_scan")
row = cur.fetchone()
return row[0] if row and row[0] else None
finally:
c.close()
def count_for_date(trade_date: str | None = None, combo_id: str | None = None) -> int:
td = trade_date or latest_trade_date(combo_id)
if not td:
return 0
c = _conn()
try:
if combo_id:
cur = c.execute(
"SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=? AND combo_id=?",
(td, combo_id),
)
else:
cur = c.execute("SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=?", (td,))
return int(cur.fetchone()[0])
finally:
c.close()
def query_scan(
*,
trade_date: str | None = None,
combo_id: str | None = None,
m_cycle: str | None = None,
w_phase: str | None = None,
d_event: str | None = None,
decision_signal: str | None = None,
min_overall_score: float | None = None,
min_alignment: float | None = None,
sort: str = "overall_score",
limit: int = 100,
offset: int = 0,
) -> list[dict[str, Any]]:
cid = combo_id or "d_w_m"
td = trade_date or latest_trade_date(cid)
if not td:
return []
sort_col = sort if sort in {
"overall_score", "alignment", "entry_score", "trend_score", "structure_score", "stars"
} else "overall_score"
clauses = ["trade_date=?", "combo_id=?"]
args: list[Any] = [td, cid]
if m_cycle:
clauses.append("m_cycle=?")
args.append(m_cycle)
if w_phase:
clauses.append("w_phase=?")
args.append(w_phase)
if d_event:
clauses.append("d_current_event=?")
args.append(d_event)
if decision_signal:
clauses.append("decision_signal=?")
args.append(decision_signal)
if min_overall_score is not None:
clauses.append("overall_score>=?")
args.append(min_overall_score)
if min_alignment is not None:
clauses.append("alignment>=?")
args.append(min_alignment)
where = " AND ".join(clauses)
args.extend([limit, offset])
c = _conn()
try:
cur = c.execute(
f"SELECT * FROM wyckoff_scan WHERE {where} ORDER BY {sort_col} DESC LIMIT ? OFFSET ?",
args,
)
return [dict(r) for r in cur.fetchall()]
finally:
c.close()
def get_symbol(
ts_code: str,
trade_date: str | None = None,
combo_id: str | None = None,
) -> dict[str, Any] | None:
cid = combo_id or "d_w_m"
td = trade_date or latest_trade_date(cid)
if not td:
return None
c = _conn()
try:
cur = c.execute(
"SELECT * FROM wyckoff_scan WHERE trade_date=? AND combo_id=? AND ts_code=?",
(td, cid, ts_code),
)
row = cur.fetchone()
return dict(row) if row else None
finally:
c.close()

Some files were not shown because too many files have changed in this diff Show More