11 币的开仓时刻:不是同时开,但同时开的那批 100% 同向
用户问 11 个币的开仓时间差距。1161 笔 / 208 天 / 实盘口径(深色 ∧ ATR≥8bp): 相邻两笔间隔中位 122 分钟,62.8% 超过 1 小时,每天仅 5.58 笔。 同时持仓数:90.9% 的时间空仓,7.5% 只有 1 仓,≥2 仓合计 1.7%,峰值 8。 所以绝大多数时候不会撞车,但左尾是硬的:7.4% 与前一笔同分钟、20.7% 在 5 分钟内。而同一分钟出现多笔的 72 个时刻里,方向完全一致的占 100% (§3.31 此前测到的是 92.7%,全样本下更极端)。 这批同时开的仓不是分散,是同一笔押注被拆到几个币上做——名义 3 个仓位, 实质 3 倍单向敞口。由此两条:保证金不是约束(91% 时间空仓,峰值并发只占 0.1% 的时间),真问题是资金闲置;并发上限必须按同向净敞口设,按仓位个数 设等于默许成倍的单向敞口。 另:TRX 在实盘口径下 208 天只有 5 笔,ATR 门控几乎全刷掉,应从币池剔除, 实际可用是 10 个币。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -241,6 +241,28 @@ push = z_above if direction == 1 else z_below
|
|||||||
> **亏损扎堆已经体现在里面**,不是用独立假设推出来的。相关性没被漏掉,
|
> **亏损扎堆已经体现在里面**,不是用独立假设推出来的。相关性没被漏掉,
|
||||||
> 只是不能再指望「加币能降回撤」。
|
> 只是不能再指望「加币能降回撤」。
|
||||||
|
|
||||||
|
**11 币全样本复核(step48,2026-08-28)**:1161 笔 / 208 天 / 实盘口径。
|
||||||
|
|
||||||
|
相邻两笔的间隔:中位 **122 分钟**,62.8% 超过 1 小时,组合平均每天仅 5.58 笔。
|
||||||
|
同时持仓数:**90.9% 的时间空仓**,7.5% 只有 1 仓,≥2 仓合计 1.7%,峰值 8。
|
||||||
|
|
||||||
|
所以「11 个币会不会同时开」的答案是:**绝大多数时候不会,但左尾是硬的**——
|
||||||
|
7.4% 的信号与前一笔同分钟、20.7% 在 5 分钟内。而这批的方向:
|
||||||
|
|
||||||
|
> 同一分钟出现多笔的时刻共 72 个,**方向完全一致的占 100.0%**(不是 92.7%)。
|
||||||
|
|
||||||
|
**这些同时开的仓不是分散,是同一笔押注被拆到几个币上做。**
|
||||||
|
名义 3 个仓位,实质是 3 倍单向敞口。两条推论:
|
||||||
|
|
||||||
|
1. **保证金不是约束,闲置才是。** 91% 时间空仓,峰值并发只占 0.1% 的时间。
|
||||||
|
「币多了保证金不够」基本不成立,真问题是资金绝大部分时间在睡觉
|
||||||
|
2. **并发上限要按「同向净敞口」设,不能按「仓位个数」设。**
|
||||||
|
同时开的必然同向,限「最多 3 仓」等于允许 3 倍单向敞口
|
||||||
|
|
||||||
|
**TRX 应从币池剔除**:实盘口径 208 天只有 **5 笔**,ATR 门控几乎全刷掉
|
||||||
|
(与 §5.3「2026 只剩 4.2%」一致)。实际可用是 10 个币,不是 11 个。
|
||||||
|
逐币每天笔数:DOGE/AVAX/SOL 约 0.66,BTC/LTC 0.45,BNB 0.31,TRX 0.02。
|
||||||
|
|
||||||
#### 3.32 多空对称:这不是伪装成策略的 beta
|
#### 3.32 多空对称:这不是伪装成策略的 beta
|
||||||
|
|
||||||
用户问「整个市场都是正相关的,是不是很少有独立行情」。市场相关是真的,
|
用户问「整个市场都是正相关的,是不是很少有独立行情」。市场相关是真的,
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Step 48:11 个币的开仓时刻到底挤不挤 —— 保证金要备多少、分散是不是真的。
|
||||||
|
|
||||||
|
用户问:11 个币对开仓时间差距有多少,不是同时开吧。
|
||||||
|
|
||||||
|
这个数决定三件事:
|
||||||
|
1. 保证金峰值。若真是同时开 11 个,按每笔固定风险算的名义额会叠到很高
|
||||||
|
2. "多币 = 分散"是否成立。§3.31 已知同时发生的信号 92.7% 同向——
|
||||||
|
那种情况下多开的仓不是分散,是加杠杆
|
||||||
|
3. 资金利用率。若大部分时间空仓,那"资金量不够"就不是真约束
|
||||||
|
|
||||||
|
口径与实盘一致:深色(同向 ∧ 阶梯)∧ ATR≥8bp,持仓按回测实际出场根数
|
||||||
|
(1m 上 1 根 = 1 分钟),不是一律按 48 根上限。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
|
||||||
|
os.environ.setdefault(v, "1")
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
sys.path.insert(0, str(HERE.parent))
|
||||||
|
pd.set_option("display.width", 320)
|
||||||
|
|
||||||
|
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
|
||||||
|
GATE_BP = 8.0
|
||||||
|
OUT = HERE / "out" / "step48_signal_times.feather"
|
||||||
|
|
||||||
|
|
||||||
|
def collect(sym: str, rows: int) -> pd.DataFrame | None:
|
||||||
|
import warnings as _w
|
||||||
|
_w.filterwarnings("ignore")
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
sys.path.insert(0, str(HERE.parent))
|
||||||
|
|
||||||
|
from chanlun import TF_DF
|
||||||
|
from chanlun.analysis.fast_bsp import (
|
||||||
|
add_zone_ladder, attach_htf_agree, attach_zone_ladder,
|
||||||
|
build_htf_zones, find_fast_bsp3, htf_fx_timeline,
|
||||||
|
)
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
from lib.exit_model import cfg_name, walk_exits
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", "1m", rows)
|
||||||
|
if df is None or len(df) < 50_000:
|
||||||
|
return None
|
||||||
|
chan = TF_DF(df, 1, "1m", lean=True)
|
||||||
|
cdf = chan.dataframe
|
||||||
|
zones = build_htf_zones(cdf, "1m", chan=chan)
|
||||||
|
if zones.empty:
|
||||||
|
return None
|
||||||
|
zl = add_zone_ladder(zones.reset_index(drop=True))
|
||||||
|
sig = find_fast_bsp3(cdf, zl)
|
||||||
|
if sig.empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dh = fetch_ohlcv(f"{sym}/USDT:USDT", "5m", 10 ** 9)
|
||||||
|
ch = TF_DF(dh, 1, "5m", lean=True)
|
||||||
|
sig = attach_zone_ladder(attach_htf_agree(sig, cdf, htf_fx_timeline(ch, ch.dataframe)), zl)
|
||||||
|
|
||||||
|
res = walk_exits(cdf, sig, [SL], [RUNNER], [MAXB],
|
||||||
|
scale_at=SCALE_AT, runners=(RUNNER,), runner_stops=(RSTOP,))
|
||||||
|
cfg = cfg_name(SL, RUNNER, MAXB, RSTOP)
|
||||||
|
idx = sig["entry_idx"].to_numpy().astype(int)
|
||||||
|
atr = cdf["atr"].to_numpy(float)[idx]
|
||||||
|
close = cdf["close"].to_numpy(float)[idx]
|
||||||
|
return pd.DataFrame({
|
||||||
|
"sym": sym,
|
||||||
|
"date": cdf["date"].to_numpy()[idx],
|
||||||
|
"dir": sig["direction"].to_numpy(),
|
||||||
|
"atr_bp": atr / close * 1e4,
|
||||||
|
"htf": sig["htf_agree"].to_numpy(),
|
||||||
|
"lad": sig["ladder_ok"].to_numpy(),
|
||||||
|
"hold": res[f"{cfg}_b"].to_numpy(),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {sym} 失败: {e!r}", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def analyse(d: pd.DataFrame, label: str) -> None:
|
||||||
|
d = d.sort_values("date").reset_index(drop=True)
|
||||||
|
span = (d.date.max() - d.date.min()).total_seconds() / 86400
|
||||||
|
print("\n" + "=" * 96)
|
||||||
|
print(f"########## {label}:{d.sym.nunique()} 币 / {len(d)} 笔 / 跨 {span:.0f} 天 ##########")
|
||||||
|
print(f"组合 {len(d) / span:.2f} 笔每天 —— 平均每 {span * 24 / len(d):.1f} 小时才 1 笔")
|
||||||
|
|
||||||
|
g = d.date.diff().dt.total_seconds().dropna() / 60
|
||||||
|
print("\n相邻两笔间隔(分钟)")
|
||||||
|
qs = [(q, g.quantile(q)) for q in (0.05, 0.10, 0.25, 0.50, 0.75, 0.90)]
|
||||||
|
print(" " + " ".join(f"{int(q*100)}%:{v:.0f}" for q, v in qs))
|
||||||
|
for lab, m in (("同一分钟", g == 0), ("≤5 分钟", g <= 5),
|
||||||
|
("≤60 分钟", g <= 60), (">1 小时", g > 60)):
|
||||||
|
print(f" {lab:<9}{m.sum():>5} 次 {m.mean()*100:>5.1f}%")
|
||||||
|
|
||||||
|
# 并发持仓:按回测实际持仓根数
|
||||||
|
ev = []
|
||||||
|
for t, h in zip(d.date, d.hold):
|
||||||
|
ev.append((t, 1))
|
||||||
|
ev.append((t + pd.Timedelta(minutes=float(h)), -1))
|
||||||
|
ev.sort()
|
||||||
|
cur, prev, dur = 0, None, {}
|
||||||
|
for t, delta in ev:
|
||||||
|
if prev is not None and t > prev:
|
||||||
|
dur[cur] = dur.get(cur, 0) + (t - prev).total_seconds()
|
||||||
|
cur += delta
|
||||||
|
prev = t
|
||||||
|
tot = sum(dur.values())
|
||||||
|
print("\n同时持仓数的时间占比")
|
||||||
|
for k in sorted(dur):
|
||||||
|
if dur[k] / tot > 0.0005:
|
||||||
|
print(f" {k:>2} 个: {dur[k]/tot*100:>5.1f}%")
|
||||||
|
print(f" 最大并发 {max(dur)} 个;有仓位的时间仅占 {(1-dur.get(0,0)/tot)*100:.1f}%")
|
||||||
|
|
||||||
|
# 同时开仓时的方向一致性——这决定"多币"到底是分散还是加杠杆
|
||||||
|
same = d.groupby(d.date)["dir"].agg(["count", "nunique"])
|
||||||
|
multi = same[same["count"] > 1]
|
||||||
|
if len(multi):
|
||||||
|
agree = (multi["nunique"] == 1).mean()
|
||||||
|
print(f"\n同一分钟出现多笔的时刻 {len(multi)} 个,其中方向完全一致的占 "
|
||||||
|
f"{agree*100:.1f}% —— 这部分不是分散,是同一笔押注被拆成几个币")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--symbols", default="BTC,BNB,ETH,SOL,LINK,LTC,AVAX,XRP,DOGE,ADA,TRX")
|
||||||
|
ap.add_argument("--rows", type=int, default=300_000)
|
||||||
|
ap.add_argument("--workers", type=int, default=3)
|
||||||
|
ap.add_argument("--reuse", action="store_true", help="直接读已存的 feather")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.reuse and OUT.exists():
|
||||||
|
d = pd.read_feather(OUT)
|
||||||
|
else:
|
||||||
|
syms = [s.strip() for s in args.symbols.split(",")]
|
||||||
|
print(f"[开仓时刻分布] {len(syms)} 币 × {args.rows} 根 1m\n", flush=True)
|
||||||
|
parts = []
|
||||||
|
with ProcessPoolExecutor(max_workers=args.workers) as ex:
|
||||||
|
fut = {ex.submit(collect, s, args.rows): s for s in syms}
|
||||||
|
for i, f in enumerate(as_completed(fut), 1):
|
||||||
|
r = f.result()
|
||||||
|
print(f" [{i}/{len(syms)}] {fut[f]} {0 if r is None else len(r)}", flush=True)
|
||||||
|
if r is not None:
|
||||||
|
parts.append(r)
|
||||||
|
if not parts:
|
||||||
|
print("无结果")
|
||||||
|
return
|
||||||
|
d = pd.concat(parts, ignore_index=True)
|
||||||
|
d.to_feather(OUT)
|
||||||
|
|
||||||
|
d["date"] = pd.to_datetime(d["date"])
|
||||||
|
dark = (d["htf"] == 1.0) & d["lad"]
|
||||||
|
analyse(d[dark & (d.atr_bp >= GATE_BP)], "实盘口径:深色 ∧ ATR≥8bp")
|
||||||
|
analyse(d[dark], "对照:深色但不加 ATR 门控")
|
||||||
|
|
||||||
|
print("\n########## 逐币笔数(实盘口径)##########")
|
||||||
|
dd = d[dark & (d.atr_bp >= GATE_BP)]
|
||||||
|
t = dd.groupby("sym").agg(笔数=("date", "size"), 中位持仓分钟=("hold", "median")).sort_values("笔数", ascending=False)
|
||||||
|
span = (dd.date.max() - dd.date.min()).total_seconds() / 86400
|
||||||
|
t["每天笔数"] = (t["笔数"] / span).round(3)
|
||||||
|
print(t.to_string())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user