策略源码
'''
================================================================
KOL蒸馏共识策略 v4.0 · 发明者量化(FMZ) Python版
默认优先读取本地缓存:
/tmp/crypto_kol_quant_cache/profiles_v2.json
/tmp/crypto_kol_quant_cache/trader_composite_ic.csv
首次无缓存时自动从 GitHub 下载:
profiles_v2/*.json (99份交易员画像)
quant_factors/trader_composite_ic.csv (IC权重)
https://github.com/0xquqi/crypto-kol-quant
完整移植 crypto-kol-quant 项目:
① 实时 BTC 日线K线 (FMZ exchange.GetRecords)
② 实时宏观数据 DXY/GOLD/US2Y/SPX (Yahoo Finance)
③ 50+技术特征 + 宏观特征 (feature_engine.py)
④ 全部68个有效因子 (factor IDs 与 profiles 完全对齐)
⑤ 99交易员共识: trust_adjusted / ic_weighted / by_school
⑥ 完整交易: 模拟/实盘 + 止损止盈 + 紧急减仓 + 仪表板
================================================================
'''
import json, os, csv, math, time, io, sys, importlib, subprocess
import urllib.request
from datetime import datetime, timezone
# ================================================================
# 依赖自动安装 (兼容 FMZ Python 3.6)
# ================================================================
DEPS_DIR = "/tmp/fmz_pydeps"
def _safe_log(*args):
try:
Log(*args)
except:
try:
print(*args)
except:
pass
def _ensure_deps_dir():
try:
if not os.path.exists(DEPS_DIR):
os.makedirs(DEPS_DIR)
except Exception as e:
_safe_log("创建依赖目录失败:", str(e))
if DEPS_DIR not in sys.path:
sys.path.insert(0, DEPS_DIR)
def _run_cmd(cmd):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = p.communicate()
text = out.decode("utf-8", errors="ignore") if out else ""
if text:
for line in text.splitlines()[-20:]:
_safe_log(line)
if p.returncode != 0:
raise Exception("命令失败: {} | code={}".format(" ".join(cmd), p.returncode))
def _ensure_package(import_name, pip_spec):
_ensure_deps_dir()
importlib.invalidate_caches()
try:
return importlib.import_module(import_name)
except ImportError:
_safe_log("⚠️ 缺少依赖: {},尝试安装到 {}".format(pip_spec, DEPS_DIR))
cmd = [
sys.executable, "-m", "pip", "install",
"--no-cache-dir",
"--target", DEPS_DIR,
pip_spec
]
try:
_safe_log("执行: {}".format(" ".join(cmd)))
_run_cmd(cmd)
importlib.invalidate_caches()
if DEPS_DIR not in sys.path:
sys.path.insert(0, DEPS_DIR)
module = importlib.import_module(import_name)
_safe_log("✅ 安装成功: {}".format(pip_spec))
return module
except Exception as e:
raise Exception("依赖安装失败: {} | {}".format(pip_spec, e))
np = _ensure_package("numpy", "numpy==1.19.5")
pd = _ensure_package("pandas", "pandas==1.1.5")
# ================================================================
# GitHub 数据源 + 本地缓存
# ================================================================
GITHUB_RAW = "https://raw.githubusercontent.com/0xquqi/crypto-kol-quant/main"
GITHUB_API = "https://api.github.com/repos/0xquqi/crypto-kol-quant/contents"
CACHE_DIR = "/tmp/crypto_kol_quant_cache"
PROFILES_CACHE_FILE = os.path.join(CACHE_DIR, "profiles_v2.json")
IC_CACHE_FILE = os.path.join(CACHE_DIR, "trader_composite_ic.csv")
USE_REMOTE_LATEST = False
def _http_get(url, timeout=30):
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
return urllib.request.urlopen(req, timeout=timeout).read()
def _ensure_cache_dir():
try:
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)
except Exception as e:
Log("⚠️ 创建缓存目录失败: {}".format(e))
def _read_text(path):
with io.open(path, "r", encoding="utf-8") as f:
return f.read()
def _write_text(path, text):
_ensure_cache_dir()
with io.open(path, "w", encoding="utf-8") as f:
f.write(text)
def _read_json(path):
return json.loads(_read_text(path))
def _write_json(path, obj):
_write_text(path, json.dumps(obj, ensure_ascii=False))
# ================================================================
# FMZ 参数面板变量
# ================================================================
try: TRADE_MODE
except: TRADE_MODE = "paper"
try: SYMBOL
except: SYMBOL = "BTC_USDT.swap"
try: INIT_CAPITAL
except: INIT_CAPITAL = 10000.0
try: LONG_THRESH
except: LONG_THRESH = 0.05
try: SHORT_THRESH
except: SHORT_THRESH = -0.05
try: CLOSE_THRESH
except: CLOSE_THRESH = 0.02
try: POSITION_RATIO
except: POSITION_RATIO = 0.90
try: LEVERAGE
except: LEVERAGE = 2.0
try: STOP_LOSS_PCT
except: STOP_LOSS_PCT = 0.06
try: TAKE_PROFIT_PCT
except: TAKE_PROFIT_PCT = 0.18
try: REBALANCE_HOURS
except: REBALANCE_HOURS = 24
try: LOOKBACK_BARS
except: LOOKBACK_BARS = 600
try: MACRO_LOOKBACK_DAYS
except: MACRO_LOOKBACK_DAYS = 90
FAST_INTERVAL = 15 * 60 * 1000
MID_INTERVAL = 30 * 60 * 1000
SLOW_INTERVAL = 60 * 60 * 1000
PNL_INTERVAL = 60 * 1000
EMERGENCY_DROP = 0.05
EMERGENCY_REDUCE = 0.50
# ================================================================
# 一、宏观数据获取 (Yahoo Finance)
# ================================================================
YAHOO_SYMBOLS = {
'DXY': 'DX-Y.NYB',
'GOLD': 'GC=F',
'US2Y': '^TNX',
'SPX': '^GSPC',
}
def fetch_yahoo(yf_symbol, days=120):
end_ts = int(time.time())
start_ts = end_ts - days * 86400
url = ("https://query1.finance.yahoo.com/v8/finance/chart/{}"
"?period1={}&period2={}&interval=1d".format(yf_symbol, start_ts, end_ts))
try:
data = json.loads(_http_get(url))
r = data['chart']['result'][0]
ts = r['timestamp']
closes = r['indicators']['quote'][0]['close']
result = {}
for i, t in enumerate(ts):
c = closes[i]
if c is not None:
result[datetime.utcfromtimestamp(t).strftime('%Y-%m-%d')] = float(c)
return result
except Exception as e:
Log("⚠️ Yahoo Finance [{}] 失败: {}".format(yf_symbol, e))
return {}
def fetch_macro(days=120):
macro = {}
for name, sym in YAHOO_SYMBOLS.items():
raw = fetch_yahoo(sym, days)
if raw:
s = pd.Series(raw)
s.index = pd.to_datetime(s.index, utc=True)
macro[name] = s.sort_index()
Log("✅ 宏观[{}] {} 条 最新{}".format(name, len(s), s.index[-1].date()))
else:
Log("⚠️ 宏观[{}] 空".format(name))
return macro
# ================================================================
# 二、特征工程 (feature_engine.py)
# ================================================================
def _sma(s, n): return s.rolling(n, min_periods=1).mean()
def _ema(s, n): return s.ewm(span=n, adjust=False).mean()
def _rsi(s, n=14):
diff = s.diff()
ag = diff.clip(lower=0).ewm(alpha=1/n, min_periods=n, adjust=False).mean()
al = (-diff).clip(lower=0).ewm(alpha=1/n, min_periods=n, adjust=False).mean()
return 100 - 100 / (1 + ag / al.replace(0, np.nan))
def _macd(c, fast=12, slow=26, sig=9):
line = _ema(c, fast) - _ema(c, slow)
sl = _ema(line, sig)
return line, sl, line - sl
def _bollinger(c, n=20, k=2):
m = _sma(c, n)
sd = c.rolling(n, min_periods=1).std(ddof=0)
return m+k*sd, m, m-k*sd, 2*k*sd/m.replace(0, np.nan)
def _atr(h, l, c, n=14):
tr = pd.concat([h-l, (h-c.shift()).abs(), (l-c.shift()).abs()], axis=1).max(axis=1)
return tr.ewm(alpha=1/n, min_periods=n, adjust=False).mean()
def _adx(h, l, c, n=14):
up = h.diff()
dn = -l.diff()
pdm = np.where((up > dn) & (up > 0), up, 0.0)
mdm = np.where((dn > up) & (dn > 0), dn, 0.0)
tr = pd.concat([h-l, (h-c.shift()).abs(), (l-c.shift()).abs()], axis=1).max(axis=1)
atr_= tr.ewm(alpha=1/n, min_periods=n, adjust=False).mean()
pdi = 100 * pd.Series(pdm, index=h.index).ewm(alpha=1/n, min_periods=n, adjust=False).mean() / atr_
mdi = 100 * pd.Series(mdm, index=h.index).ewm(alpha=1/n, min_periods=n, adjust=False).mean() / atr_
dx = 100 * (pdi-mdi).abs() / (pdi+mdi).replace(0, np.nan)
return dx.ewm(alpha=1/n, min_periods=n, adjust=False).mean()
def build_features(records, macro=None):
df = pd.DataFrame(records)
if 'Time' in df.columns:
df = df.rename(columns={
'Time': 'time_ms',
'Open': 'open',
'High': 'high',
'Low': 'low',
'Close': 'close',
'Volume': 'volume'
})
elif 'time_ms' not in df.columns:
df = pd.DataFrame(records, columns=['time_ms','open','high','low','close','volume'])
df['date'] = pd.to_datetime(df['time_ms'], unit='ms', utc=True)
df = df.set_index('date').sort_index()
c, h, l, o = df['close'], df['high'], df['low'], df['open']
f = pd.DataFrame(index=df.index)
f[['close','open','high','low','volume']] = df[['close','open','high','low','volume']]
f['ma20'] = _sma(c,20); f['ma50'] = _sma(c,50)
f['ma100'] = _sma(c,100); f['ma200'] = _sma(c,200)
f['ema20'] = _ema(c,20); f['ema50'] = _ema(c,50)
f['ma_20w'] = _sma(c,140); f['ma_50w'] = _sma(c,350)
f['ma_200w']= _sma(c,1400); f['ema_50w']= _ema(c,350)
f['rsi14'] = _rsi(c,14)
f['rsi14_prev'] = f['rsi14'].shift(1)
ml, ms, mh = _macd(c)
f['macd'] = ml
f['macd_sig'] = ms
f['macd_hist'] = mh
f['macd_hist_prev'] = mh.shift(1)
bbu, bbm, bbl, bbw = _bollinger(c)
f['bb_upper'] = bbu
f['bb_mid'] = bbm
f['bb_lower'] = bbl
f['bb_width'] = bbw
f['bb_width_20pctile'] = bbw.rolling(100, min_periods=30).quantile(0.2)
f['bb_width_80pctile'] = bbw.rolling(100, min_periods=30).quantile(0.8)
f['atr14'] = _atr(h,l,c,14)
f['adx14'] = _adx(h,l,c,14)
for n in [1,5,7,14,30,60,90]:
f['ret_{}d'.format(n)] = c.pct_change(n)
logr = np.log(c / c.shift(1))
f['rv30'] = logr.rolling(30, min_periods=10).std() * np.sqrt(365)
def _last_pct_rank(x):
s = pd.Series(x)
s = s.dropna()
if len(s) == 0:
return np.nan
last = s.iloc[-1]
return float((s <= last).sum()) / float(len(s))
f['rv30_pctile'] = f['rv30'].rolling(200, min_periods=60).apply(_last_pct_rank, raw=False)
f['high_20d'] = h.rolling(20, min_periods=1).max()
f['low_20d'] = l.rolling(20, min_periods=1).min()
f['high_50d'] = h.rolling(50, min_periods=1).max()
f['low_50d'] = l.rolling(50, min_periods=1).min()
f['high_200d'] = h.rolling(200, min_periods=1).max()
f['low_200d'] = l.rolling(200, min_periods=1).min()
f['pct_from_high_50d'] = (c-f['high_50d'])/f['high_50d']
f['pct_from_high_200d'] = (c-f['high_200d'])/f['high_200d']
f['pct_from_ma_200w'] = (c-f['ma_200w'])/f['ma_200w'].replace(0,np.nan)
f['pct_from_ma_50w'] = (c-f['ma_50w'])/f['ma_50w'].replace(0,np.nan)
rng = (h-l).replace(0,np.nan)
f['body_pct'] = (c-o).abs()/rng
f['upper_wick_pct'] = (h-c.where(c>o,o))/rng
f['lower_wick_pct'] = (c.where(c<o,o)-l)/rng
f['is_green'] = (c>o).astype(int)
f['hh_count_20d'] = (h>h.shift(1)).rolling(20, min_periods=1).sum()
f['ll_count_20d'] = (l<l.shift(1)).rolling(20, min_periods=1).sum()
f['uptrend_20d'] = (f['hh_count_20d']>f['ll_count_20d']).astype(int)
f['downtrend_20d'] = (f['ll_count_20d']>f['hh_count_20d']).astype(int)
f['price_above_ma200'] = (c>f['ma200']).astype(int)
f['price_above_ma50'] = (c>f['ma50']).astype(int)
f['ma50_above_ma200'] = (f['ma50']>f['ma200']).astype(int)
f['ma50_above_ma200_prev'] = f['ma50_above_ma200'].shift(1)
f['days_below_ma200'] = (c<f['ma200']).groupby((c>=f['ma200']).cumsum()).cumcount()
sw_h = f['high_50d']
sw_l = f['low_50d']
f['fib_382'] = sw_h-0.382*(sw_h-sw_l)
f['fib_500'] = sw_h-0.500*(sw_h-sw_l)
f['fib_618'] = sw_h-0.618*(sw_h-sw_l)
f['fib_786'] = sw_h-0.786*(sw_h-sw_l)
f['month'] = df.index.month
f['day_of_week'] = df.index.dayofweek
pct_rng = (f['high_20d']-f['low_20d'])/c.replace(0,np.nan)
f['tight_range_20d'] = (pct_rng<0.08).astype(int)
f['tight_range_streak'] = f['tight_range_20d'].groupby((f['tight_range_20d']==0).cumsum()).cumcount()
typ = (h + l + c) / 3
wt = (h - l).fillna(0) + 1e-9
qstr = df.index.to_period('Q').astype(str)
grp = pd.DataFrame({'tw': typ * wt, 'w': wt, 'q': qstr}, index=df.index)
cum_tw = grp.groupby('q')['tw'].cumsum()
cum_w = grp.groupby('q')['w'].cumsum()
f['qvwap'] = cum_tw / cum_w.replace(0, np.nan)
f['ret_4y'] = c.pct_change(365*4)
f['days_below_ma_200w'] = (c<f['ma_200w']).groupby((c>=f['ma_200w']).cumsum()).cumcount()
if macro:
def _align(s):
return s.reindex(f.index, method='ffill')
if 'DXY' in macro:
dxy = _align(macro['DXY'])
f['dxy_ret_20d'] = dxy.pct_change(20)
f['dxy_trend_down'] = (dxy.pct_change(20)<-0.01).astype(int)
if 'GOLD' in macro:
f['gold_ret_20d'] = _align(macro['GOLD']).pct_change(20)
if 'US2Y' in macro:
y = _align(macro['US2Y'])
f['y_ret_20d'] = y.pct_change(20)
f['y_slope_20d_neg'] = (y.diff(20)<0).astype(int)
if 'SPX' in macro:
spx = _align(macro['SPX'])
f['spx_ret_20d'] = spx.pct_change(20)
f['spx_trend_up'] = (spx.pct_change(20)>0).astype(int)
return f
# ================================================================
# 三、因子评估
# ================================================================
HALVINGS = [
pd.Timestamp('2012-11-28', tz='UTC'),
pd.Timestamp('2016-07-09', tz='UTC'),
pd.Timestamp('2020-05-11', tz='UTC'),
pd.Timestamp('2024-04-19', tz='UTC'),
pd.Timestamp('2028-04-01', tz='UTC'),
]
def _days_since_halving(ts):
last = None
for h in HALVINGS:
if h <= ts:
last = h
return (ts - last).days if last else 9999
def _get(row, key, default=0.0):
v = row.get(key, default) if isinstance(row, dict) else getattr(row, key, default)
if v is None or (isinstance(v, float) and math.isnan(v)):
return default
return float(v)
def _safe_int(v, default=0):
try:
if v is None:
return default
if isinstance(v, float) and math.isnan(v):
return default
return int(v)
except:
return default
def _prev(f, col, n=1):
if col not in f.columns:
return 0.0
if len(f) > n:
v = f[col].shift(n).iloc[-1]
return float(v) if v is not None and not (isinstance(v, float) and math.isnan(v)) else 0.0
return 0.0
def _rmax(f, col, n, sh=0):
s = f[col].rolling(n, min_periods=max(1, n//3)).max()
if sh:
s = s.shift(sh)
v = s.iloc[-1]
return float(v) if not (isinstance(v, float) and math.isnan(v)) else 0.0
def _rmin(f, col, n, sh=0):
s = f[col].rolling(n, min_periods=max(1, n//3)).min()
if sh:
s = s.shift(sh)
v = s.iloc[-1]
return float(v) if not (isinstance(v, float) and math.isnan(v)) else 0.0
def evaluate_factors(f):
row = f.iloc[-1]
now_ts = f.index[-1]
s = {}
close = _get(row,'close')
o_val = _get(row,'open',close)
h_val = _get(row,'high',close)
l_val = _get(row,'low',close)
rsi14 = _get(row,'rsi14',50)
adx14 = _get(row,'adx14',0)
ret1 = _get(row,'ret_1d',0)
ret5 = _get(row,'ret_5d',0)
ret7 = _get(row,'ret_7d',0)
ret30 = _get(row,'ret_30d',0)
rv30 = _get(row,'rv30',0.5)
month = _safe_int(_get(row,'month',1), 1)
dow = _safe_int(_get(row,'day_of_week',0), 0)
is_green = bool(_get(row,'is_green',0))
above200 = bool(_get(row,'price_above_ma200',0))
ma50_gt = bool(_get(row,'ma50_above_ma200',0))
ma50_prev= bool(_get(row,'ma50_above_ma200_prev',0))
high_20d = _get(row,'high_20d',close)
low_20d = _get(row,'low_20d',close)
high_50d = _get(row,'high_50d',close)
low_50d = _get(row,'low_50d',close)
ma_200w = _get(row,'ma_200w',0)
ma_20w = _get(row,'ma_20w',0)
ma50_v = _get(row,'ma50',close)
days_below = _safe_int(_get(row,'days_below_ma200',0), 0)
streak = _safe_int(_get(row,'tight_range_streak',0), 0)
mh = _get(row,'macd_hist',0)
mhp = _get(row,'macd_hist_prev',0)
bb_u = _get(row,'bb_upper',close*1.02)
bb_l = _get(row,'bb_lower',close*0.98)
bb_w = _get(row,'bb_width',0.05)
bb_w20 = _get(row,'bb_width_20pctile',0.04)
fib618 = _get(row,'fib_618',0)
qvwap = _get(row,'qvwap',0)
body_pct = _get(row,'body_pct',0.5)
upper_wick = _get(row,'upper_wick_pct',0)
lower_wick = _get(row,'lower_wick_pct',0)
hh20 = _get(row,'hh_count_20d',0)
ll20 = _get(row,'ll_count_20d',0)
uptrend20 = bool(_get(row,'uptrend_20d',0))
ret4y = _get(row,'ret_4y', np.nan)
rv30_pctile = _get(row,'rv30_pctile',0.5)
rsi_prev = _prev(f,'rsi14',1)
close_p1 = _prev(f,'close',1)
open_p1 = _prev(f,'open',1)
close_5ago = _prev(f,'close',5)
close_20ago = _prev(f,'close',20)
rsi14_20ago = _prev(f,'rsi14',20)
rv30_20ago = _prev(f,'rv30',20)
rv30_30ago = _prev(f,'rv30',30)
bb_w_p1 = _prev(f,'bb_width',1)
bb_w20_p1 = _prev(f,'bb_width_20pctile',1)
h20d_p1 = _prev(f,'high_20d',1)
l20d_p1 = _prev(f,'low_20d',1)
h20d_p2 = _prev(f,'high_20d',2)
l20d_p2 = _prev(f,'low_20d',2)
h50d_p1 = _prev(f,'high_50d',1)
l50d_p1 = _prev(f,'low_50d',1)
h50d_10 = _prev(f,'high_50d',10)
l20d_p5 = _prev(f,'low_20d',5)
h20d_p6 = _prev(f,'high_20d',6)
close_p4 = _prev(f,'close',4)
h_p2 = _prev(f,'high',2)
l_p2 = _prev(f,'low',2)
ma_20w_p1 = _prev(f,'ma_20w',1)
ma_20w_p5 = _prev(f,'ma_20w',5)
ret5_5ago = _prev(f,'ret_5d',5)
close_60ago = _prev(f,'close',60)
low_200d_60 = _prev(f,'low_200d',60)
bb_w_20ago = _prev(f,'bb_width',20)
h20d_10ago = _prev(f,'high_20d',10)
l20d_10ago = _prev(f,'low_20d',10)
dxy_r20 = _get(row,'dxy_ret_20d', np.nan)
gold_r20 = _get(row,'gold_ret_20d', np.nan)
y_r20 = _get(row,'y_ret_20d', np.nan)
spx_r20 = _get(row,'spx_ret_20d', np.nan)
def _nm(v):
return isinstance(v, float) and math.isnan(v)
# ── PATTERN (23) ──
h5d_p1 = _rmax(f,'high',5,sh=1)
s['cap_001_falling_wedge_breakout'] = (
0.65 if (ret30<-0.05 and rv30<rv30_20ago*0.9 and rv30_20ago>0 and close>h5d_p1) else 0.0)
l5d_p1 = _rmin(f,'low',5,sh=1)
s['cap_002_rising_wedge_breakdown'] = (
-0.60 if (ret30>0.05 and rv30<rv30_20ago*0.9 and rv30_20ago>0 and close<l5d_p1) else 0.0)
h3d_p1 = _rmax(f,'high',3,sh=1)
consol = abs(ret5)<0.03 and adx14<20
s['cap_003_bull_flag'] = 0.7 if (ret5_5ago>0.10 and consol and close>h3d_p1) else 0.0
l3d_p1 = _rmin(f,'low',3,sh=1)
s['cap_004_bear_flag'] = -0.7 if (ret5_5ago<-0.10 and consol and close<l3d_p1) else 0.0
if len(f) >= 20:
try:
hage_raw = f['high'].rolling(60, min_periods=20).apply(
lambda x: float(np.argmax(x)), raw=True
).iloc[-1]
hage = _safe_int(60 - hage_raw, 0)
except:
hage = 0
else:
hage = 0
s['cap_005_head_shoulders_top'] = -0.5 if (close<low_20d and hage>10 and rsi14<50) else 0.0
if len(f) >= 20:
try:
lage_raw = f['low'].rolling(60, min_periods=20).apply(
lambda x: float(np.argmin(x)), raw=True
).iloc[-1]
lage = _safe_int(60 - lage_raw, 0)
except:
lage = 0
else:
lage = 0
s['cap_006_inverse_head_shoulders'] = 0.5 if (close>high_20d and lage>10 and rsi14>50) else 0.0
pk_r = _rmax(f,'high',15)
pk_p = _rmax(f,'high',15,sh=15)
eq_pk = abs(pk_r-pk_p)/max(pk_r,1)<0.03 if pk_p>0 else False
s['cap_007_double_top'] = -0.6 if (eq_pk and close<low_20d) else 0.0
lw_r = _rmin(f,'low',15)
lw_p = _rmin(f,'low',15,sh=15)
eq_lw = abs(lw_r-lw_p)/max(abs(lw_r),1)<0.03 if lw_p>0 else False
s['cap_008_double_bottom'] = 0.6 if (eq_lw and close>high_20d) else 0.0
h90p1 = _rmax(f,'high',90,sh=1)
s['cap_009_cup_and_handle'] = (
0.5 if (close_60ago<low_200d_60*1.1 and low_200d_60>0
and 0<rv30_30ago<0.5 and close>h90p1) else 0.0)
flat_h = abs(high_20d-h20d_10ago)/max(high_20d,1)<0.02 if h20d_10ago>0 else False
s['cap_010_ascending_triangle'] = 0.6 if (flat_h and low_20d>l20d_10ago and close>high_20d) else 0.0
flat_l = abs(low_20d-l20d_10ago)/max(abs(low_20d),1)<0.02 if l20d_10ago>0 else False
s['cap_011_descending_triangle'] = -0.6 if (flat_l and high_20d<h20d_10ago and close<low_20d) else 0.0
s['cap_012_sfp_swing_failure'] = (
-0.6 if (h_val>h20d_p1 and close<h20d_p1) else
0.6 if (l_val<l20d_p1 and close>l20d_p1) else 0.0)
rng_20 = high_20d - low_20d
rp_20 = (close-low_20d)/rng_20 if rng_20>0 else 0.5
s['cap_013_range_fade_high_low'] = (
-0.5 if (adx14<20 and rp_20>0.85) else
0.5 if (adx14<20 and rp_20<0.15) else 0.0)
near_ma50 = abs(close-ma50_v)/close<0.02 if close>0 else False
s['cap_014_trend_pullback_continuation'] = 0.6 if (ma50_gt and near_ma50 and is_green) else 0.0
if len(f)>=10:
thr_hh = bool((f['high']>f['high'].shift(1)).rolling(10,min_periods=3).sum().iloc[-1]>=5)
else:
thr_hh = False
s['cap_050_three_drives_pattern'] = -0.5 if (thr_hh and rsi14>70) else 0.0
h30_6 = _rmax(f,'high',30,sh=6)
ll_20_1 = _rmin(f,'low',20,sh=1)
s['cap_051_quasimodo'] = -0.55 if (close_5ago>h30_6 and h30_6>0 and close<ll_20_1 and ll_20_1>0) else 0.0
h10p1 = _rmax(f,'high',10,sh=1)
l10p1 = _rmin(f,'low',10,sh=1)
s['cap_052_liquidity_grab_sweep'] = (
0.65 if (l_val<l10p1 and lower_wick>0.5 and is_green) else
-0.65 if (h_val>h10p1 and upper_wick>0.5 and not is_green) else 0.0)
s['cap_053_doji_reversal'] = 0.0
b_now = abs(close-o_val)
b_prev = abs(close_p1-open_p1)
bull_eng = b_now>b_prev and is_green and o_val<close_p1 and close>open_p1
bear_eng = b_now>b_prev and not is_green and o_val>close_p1 and close<open_p1
s['cap_054_engulfing_reversal'] = 0.55 if bull_eng else (-0.55 if bear_eng else 0.0)
rng_v = h_val-l_val if h_val>l_val else 1.0
b_raw = abs(close-o_val)
up_pin = upper_wick>0.6 and b_raw<rng_v*0.25 and abs(h_val-high_20d)/max(h_val,1)<0.01
lo_pin = lower_wick>0.6 and b_raw<rng_v*0.25 and abs(l_val-low_20d)/max(abs(l_val),1)<0.01
s['cap_055_pin_bar_rejection'] = -0.55 if up_pin else (0.55 if lo_pin else 0.0)
if len(f)>=10:
bwc = int((f['lower_wick_pct']>0.4).rolling(10,min_periods=2).sum().iloc[-1])
else:
bwc=0
s['cap_056_double_needle_bottom'] = 0.55 if bwc>=2 else 0.0
fb_up = close_p1>h20d_p2 and close<h20d_p2 if h20d_p2>0 else False
fb_dn = close_p1<l20d_p2 and close>l20d_p2 if l20d_p2>0 else False
s['cap_057_fake_breakout_trap'] = -0.55 if fb_up else (0.55 if fb_dn else 0.0)
if len(f)>=45:
m30s = f['low'].rolling(30,min_periods=5).min()
ec = ((f['low']-m30s).abs()/m30s.clip(lower=1)<0.03).rolling(45,min_periods=3).sum()
tc = float(ec.iloc[-1])
else:
tc=0.0
s['cap_058_three_bottom_structure'] = 0.6 if (tc>=3 and close>high_20d) else 0.0
# ── STRUCTURAL (8) ──
s['cap_023_elliott_wave_3'] = 0.6 if (ret30>0.20 and 55<=rsi14<=80 and hh20>ll20) else 0.0
s['cap_024_wyckoff_accumulation'] = (
0.75 if (close<low_50d*1.03 and l_val<l50d_p1 and l50d_p1>0
and close>l50d_p1 and lower_wick>0.4) else 0.0)
s['cap_025_wyckoff_distribution'] = (
-0.75 if (close>high_50d*0.97 and h_val>h50d_p1 and h50d_p1>0
and close<h50d_p1 and upper_wick>0.4) else 0.0)
was_bo = close_5ago > h20d_p6 if h20d_p6>0 else False
rtest = abs(close-close_5ago)/max(close_5ago,1)<0.02 if close_5ago>0 else False
s['cap_026_smc_order_block'] = 0.5 if (was_bo and rtest) else 0.0
s['cap_048_ict_breaker_block'] = (
-0.5 if (close_p4<l20d_p5 and l20d_p5>0
and abs(close-l20d_p5)/max(l20d_p5,1)<0.02) else 0.0)
s['cap_049_ict_fair_value_gap'] = (
0.5 if (h_p2>0 and l_val>h_p2) else
-0.5 if (l_p2>0 and h_val<l_p2) else 0.0)
s['cap_065_btc_dominance_shift'] = 0.0
s['cap_hh_defense'] = 0.55 if (low_20d>low_50d and uptrend20 and close>low_20d*1.02) else 0.0
# ── INDICATOR (9) ──
s['cap_015_rsi_bullish_divergence'] = (
0.7 if (close<close_20ago and close<low_20d and rsi14>rsi14_20ago) else 0.0)
s['cap_016_rsi_bearish_divergence'] = (
-0.7 if (close>close_20ago and close>high_20d and rsi14<rsi14_20ago) else 0.0)
s['cap_017_rsi_oversold_bounce'] = 0.6 if (rsi14>30 and rsi_prev<=30) else 0.0
s['cap_018_ma_golden_cross'] = 0.6 if (ma50_gt and not ma50_prev) else 0.0
s['cap_019_ma_death_cross'] = -0.6 if (not ma50_gt and ma50_prev) else 0.0
s['cap_020_macd_histogram_cross'] = (
0.5 if (mh>0 and mhp<=0) else -0.5 if (mh<0 and mhp>=0) else 0.0)
squeezed = bb_w_p1<bb_w20_p1 if bb_w20_p1>0 else False
s['cap_021_bollinger_squeeze_breakout'] = (
0.6 if (squeezed and close>bb_u) else
-0.6 if (squeezed and close<bb_l) else 0.0)
if fib618>0 and abs(close-fib618)/close<0.02:
s['cap_022_fib_618_retracement_support'] = 0.55 if is_green else 0.0
else:
s['cap_022_fib_618_retracement_support'] = 0.0
s['cap_069_moving_average_reclaim'] = 0.65 if (above200 and days_below>=3) else 0.0
# ── REGIME (8) ──
days_h = _days_since_halving(now_ts)
s['cap_037_halving_cycle_phase'] = (
0.6 if 180<=days_h<540 else
-0.5 if 540<=days_h<720 else
-0.3 if days_h>=720 else 0.0)
s['cap_038_4year_cycle'] = (
0.5 if 180<=days_h<540 else
-0.4 if 540<=days_h<1080 else 0.0)
s['cap_041_dont_catch_falling_knives'] = -0.55 if (ret1<-0.08 and not is_green) else 0.0
s['cap_044_regime_trending_up'] = 1.0 if (above200 and ma50_gt and adx14>25) else 0.0
s['cap_045_regime_trending_down'] = 1.0 if (not above200 and not ma50_gt and adx14>25) else 0.0
if adx14<20 and bb_w20>0 and bb_w<bb_w20:
rp46 = max(0.0, min(1.0, (close-low_20d)/rng_20)) if rng_20>0 else 0.5
s['cap_046_regime_ranging'] = float((0.5-rp46)*2)
else:
s['cap_046_regime_ranging'] = 0.0
s['cap_047_regime_volatile'] = 0.0
s['cap_070_parabolic_exhaustion'] = -1.0 if (ret7>0.25 and rsi14>80) else 0.0
# ── MACRO (4) ──
s['cap_027_dxy_inverse_btc'] = 0.4 if (not _nm(dxy_r20) and dxy_r20<-0.01) else 0.0
s['cap_028_spx_risk_on_off'] = 0.4 if (not _nm(spx_r20) and spx_r20>0.02) else 0.0
s['cap_029_yields_liquidity'] = 0.4 if (not _nm(y_r20) and y_r20<-0.02) else 0.0
s['cap_030_gold_flight_to_safety'] = 0.3 if (not _nm(gold_r20) and gold_r20>0.05) else 0.0
# ── MOCK (19) ──
for mid in ['cap_031_funding_extreme_negative','cap_032_funding_extreme_positive',
'cap_033_oi_climb_price_flat','cap_034_liquidation_cluster_magnet',
'cap_035_exchange_inflow_bearish','cap_036_lth_holding',
'cap_039_fomc_risk_off','cap_040_etf_flows_proxy',
'cap_042_position_sizing_cap','cap_043_cut_losses_early',
'cap_059_funding_divergence','cap_060_basis_blowout',
'cap_061_options_skew_bullish','cap_062_m2_growth_proxy',
'cap_063_ism_pmi_cycle','cap_064_credit_spreads_widening',
'cap_066_stablecoin_supply_growth','cap_067_nvt_valuation_extreme',
'cap_068_mvrv_zscore_cycle']:
s[mid] = 0.0
return s
# ================================================================
# 四、Profile & IC 加载 (默认优先本地缓存)
# ================================================================
_profiles_cache = None
_ic_cache = None
def load_profiles():
global _profiles_cache
if _profiles_cache is not None:
return _profiles_cache
profiles = []
if (not USE_REMOTE_LATEST) and os.path.exists(PROFILES_CACHE_FILE):
try:
profiles = _read_json(PROFILES_CACHE_FILE)
_profiles_cache = profiles
Log("✅ Profiles 使用本地缓存: {} 份".format(len(profiles)))
return profiles
except Exception as e:
Log("⚠️ 读取 Profiles 本地缓存失败: {}".format(e))
Log("━━ 从GitHub下载交易员Profiles ━━")
try:
items = json.loads(_http_get("{}/profiles_v2".format(GITHUB_API)))
jfiles = [i for i in items if i['name'].endswith('.json')]
Log("发现 {} 份Profile,开始下载...".format(len(jfiles)))
ok = 0
for item in jfiles:
try:
p = json.loads(_http_get(item['download_url']))
caps = {c['id']: float(c.get('weight', 0.5))
for c in p.get('capabilities_used', [])}
profiles.append({
'handle': p.get('handle', item['name'][:-5]),
'caps': caps
})
ok += 1
except Exception as e:
Log("⚠️ Profile[{}] 失败: {}".format(item['name'], e))
Log("✅ Profile下载完成: {}/{} 份".format(ok, len(jfiles)))
if profiles:
try:
_write_json(PROFILES_CACHE_FILE, profiles)
Log("💾 Profiles 已写入本地缓存")
except Exception as e:
Log("⚠️ 写入 Profiles 缓存失败: {}".format(e))
except Exception as e:
Log("⚠️ GitHub目录请求失败: {}".format(e))
if os.path.exists(PROFILES_CACHE_FILE):
try:
profiles = _read_json(PROFILES_CACHE_FILE)
Log("⚠️ 改用旧的 Profiles 本地缓存: {} 份".format(len(profiles)))
except Exception as e2:
Log("⚠️ 旧 Profiles 缓存也不可用: {}".format(e2))
_profiles_cache = profiles
return profiles
def load_ic():
global _ic_cache
if _ic_cache is not None:
return _ic_cache
ic = {}
if (not USE_REMOTE_LATEST) and os.path.exists(IC_CACHE_FILE):
try:
raw = _read_text(IC_CACHE_FILE)
for row in csv.DictReader(raw.splitlines()):
try:
ic[row['handle']] = {
'ic_30d': float(row.get('ic_30d', 0) or 0),
'school': row.get('school', '') or '',
'bias_default': row.get('bias_default', '') or '',
}
except:
pass
_ic_cache = ic
Log("✅ IC 使用本地缓存: {} 条".format(len(ic)))
return ic
except Exception as e:
Log("⚠️ 读取 IC 本地缓存失败: {}".format(e))
Log("━━ 从GitHub下载IC数据 ━━")
try:
raw = _http_get("{}/quant_factors/trader_composite_ic.csv".format(GITHUB_RAW)).decode('utf-8')
for row in csv.DictReader(raw.splitlines()):
try:
ic[row['handle']] = {
'ic_30d': float(row.get('ic_30d', 0) or 0),
'school': row.get('school', '') or '',
'bias_default': row.get('bias_default', '') or '',
}
except:
pass
Log("✅ IC数据下载完成: {} 条".format(len(ic)))
if ic:
try:
_write_text(IC_CACHE_FILE, raw)
Log("💾 IC 已写入本地缓存")
except Exception as e:
Log("⚠️ 写入 IC 缓存失败: {}".format(e))
except Exception as e:
Log("⚠️ IC数据下载失败: {}".format(e))
if os.path.exists(IC_CACHE_FILE):
try:
raw = _read_text(IC_CACHE_FILE)
for row in csv.DictReader(raw.splitlines()):
try:
ic[row['handle']] = {
'ic_30d': float(row.get('ic_30d', 0) or 0),
'school': row.get('school', '') or '',
'bias_default': row.get('bias_default', '') or '',
}
except:
pass
Log("⚠️ 改用旧的 IC 本地缓存: {} 条".format(len(ic)))
except Exception as e2:
Log("⚠️ 旧 IC 缓存也不可用: {}".format(e2))
_ic_cache = ic
return ic
# ================================================================
# 五、共识计算
# ================================================================
def compute_consensus(factor_scores):
profiles = load_profiles()
ic_data = load_ic()
if not profiles:
return {'trust_adjusted':0.0,'ic_weighted':0.0,'equal_mean':0.0,
'long_count':0,'short_count':0,'neutral_count':0,
'top_factors':[],'by_school':{},'top_traders':[],'reverse_traders':[]}
trader_signals = []
for p in profiles:
sig = 0.0
wt = 0.0
for cap_id, w in p['caps'].items():
score = factor_scores.get(cap_id, 0.0)
sig += w * score
wt += abs(w)
trader_raw = sig / wt if wt > 0 else 0.0
ic_info = ic_data.get(p['handle'], {})
trader_signals.append({
'handle': p['handle'],
'signal': trader_raw,
'ic': ic_info.get('ic_30d', 0.0),
'school': ic_info.get('school', 'unknown') or 'unknown',
'bias_default': ic_info.get('bias_default', ''),
})
long_n = sum(1 for t in trader_signals if t['signal'] > 0.03)
short_n = sum(1 for t in trader_signals if t['signal'] < -0.03)
neut_n = len(trader_signals) - long_n - short_n
eq_mean = sum(t['signal'] for t in trader_signals) / max(len(trader_signals), 1)
pos_w = sum(max(t['ic'], 0) for t in trader_signals)
ic_wt = (sum(t['signal']*max(t['ic'],0) for t in trader_signals)/pos_w
if pos_w > 0 else 0.0)
abs_w = sum(abs(t['ic']) for t in trader_signals)
trust = (sum((t['signal'] if t['ic']>=0 else -t['signal'])*abs(t['ic'])
for t in trader_signals)/abs_w if abs_w > 0 else 0.0)
top_factors = sorted([(k,v) for k,v in factor_scores.items() if abs(v)>0.05],
key=lambda x:abs(x[1]), reverse=True)[:10]
by_school = {}
for t in trader_signals:
sc = t['school'] or 'unknown'
if sc not in by_school:
by_school[sc] = {'count':0,'long':0,'short':0,'neutral':0,'signals':[]}
by_school[sc]['count'] += 1
by_school[sc]['signals'].append(t['signal'])
if t['signal'] > 0.03: by_school[sc]['long'] += 1
elif t['signal'] < -0.03: by_school[sc]['short'] += 1
else: by_school[sc]['neutral'] += 1
for sc, d in by_school.items():
d['mean_signal'] = sum(d['signals']) / max(len(d['signals']), 1)
top_traders = sorted([t for t in trader_signals if t['ic'] > 0.05],
key=lambda x:x['ic'], reverse=True)[:15]
reverse_traders = sorted([t for t in trader_signals if t['ic'] < -0.05],
key=lambda x:x['ic'])[:10]
return {'trust_adjusted':trust,'ic_weighted':ic_wt,'equal_mean':eq_mean,
'long_count':long_n,'short_count':short_n,'neutral_count':neut_n,
'top_factors':top_factors,'by_school':by_school,
'top_traders':top_traders,'reverse_traders':reverse_traders}
# ================================================================
# 六、交易执行
# ================================================================
def safe_get_cash():
v = _G('pt_cash')
return float(INIT_CAPITAL) if v is None else float(v)
def calc_equity(price):
cash = safe_get_cash()
pos = json.loads(_G('pt_position') or 'null')
if not pos:
return cash
unreal = (pos['margin']+(price-pos['entry_price'])*pos['qty'] if pos['side']=='long'
else pos['margin']+(pos['entry_price']-price)*pos['qty'])
return cash + unreal
def paper_open(side, price, signal_val):
equity = calc_equity(price)
try:
pr = float(POSITION_RATIO)
lv = float(LEVERAGE)
except:
pr = 0.9
lv = 2.0
strength = min(abs(signal_val)*4, 1.0)
nominal = equity * pr * lv * strength
margin = nominal / lv
cash = safe_get_cash()
if margin > cash-1:
margin = max(cash-1, 0)
nominal = margin * lv
if margin < 10:
Log("⚠️ 保证金不足10U")
return False
qty = nominal / price
cash -= margin
pos = {'side':side,'entry_price':price,'qty':qty,'margin':margin,
'leverage':lv,'open_time':_D()}
_append_log({'action':'open','side':side,'price':round(price,4),
'qty':round(qty,6),'margin':round(margin,2),'time':_D()})
_G('pt_position',json.dumps(pos))
_G('pt_cash',cash)
Log("📂 开{}仓 价:{:.4f} qty:{:.6f} 保:{:.2f}U sig:{:.4f}".format(side,price,qty,margin,signal_val))
return True
def paper_close(price, reason="signal"):
pos = json.loads(_G('pt_position') or 'null')
if not pos:
return False
cash = safe_get_cash()
realized = float(_G('pt_realized_pnl') or 0)
pnl = ((price-pos['entry_price'])*pos['qty'] if pos['side']=='long'
else (pos['entry_price']-price)*pos['qty'])
cash += pos['margin'] + pnl
realized += pnl
_append_log({'action':'close','side':pos['side'],'reason':reason,
'entry':round(pos['entry_price'],4),'close':round(price,4),
'pnl':round(pnl,4),'time':_D()})
_G('pt_position',json.dumps(None))
_G('pt_cash',cash)
_G('pt_realized_pnl',realized)
Log("📤 平{}仓{} 开:{:.4f} 平:{:.4f} PnL:${:.4f} [{}]".format(
pos['side'],'🟢' if pnl>=0 else '🔴',pos['entry_price'],price,pnl,reason))
return True
def emergency_reduce_paper(price, side, ratio=0.5):
pos = json.loads(_G('pt_position') or 'null')
if not pos:
return
rq = pos['qty'] * ratio
rm = pos['margin'] * ratio
pnl = ((price-pos['entry_price'])*rq if side=='long' else (pos['entry_price']-price)*rq)
cash = safe_get_cash() + rm + pnl
realized = float(_G('pt_realized_pnl') or 0) + pnl
pos['qty'] -= rq
pos['margin'] -= rm
_append_log({'action':'emergencyReduce','side':side,'price':round(price,4),
'pnl':round(pnl,4),'ratio':ratio,'time':_D()})
_G('pt_position',json.dumps(None) if pos['qty']<1e-8 else json.dumps(pos))
_G('pt_cash',cash)
_G('pt_realized_pnl',realized)
Log("🚨 紧急减仓[{}] {:.0f}% PnL:${:.4f}".format(side,ratio*100,pnl))
def live_open(side, price):
try:
acc = exchange.GetAccount()
if not acc:
return False
try:
pr = float(POSITION_RATIO)
lv = float(LEVERAGE)
except:
pr = 0.9
lv = 2.0
equity = float(acc.get('Balance',0)) + float(acc.get('FrozenBalance',0))
nominal = equity * pr * lv
qty = nominal / price
if nominal < 10:
return False
oid = exchange.CreateOrder(SYMBOL,'buy' if side=='long' else 'sell',-1,qty)
Log("✅ 实盘开{}仓 oid:{} qty:{:.6f}".format(side,oid,qty))
return bool(oid)
except Exception as e:
Log("⚠️ 实盘开仓异常:", str(e))
return False
def live_close(reason="signal"):
try:
positions = exchange.GetPositions(SYMBOL)
if not positions:
return False
for p in positions:
if p['Amount']<=0:
continue
oid = exchange.CreateOrder(SYMBOL,'closebuy' if p['Type']==0 else 'closesell',-1,p['Amount'])
Log("✅ 实盘平仓 oid:{} [{}]".format(oid,reason))
return True
except Exception as e:
Log("⚠️ 实盘平仓异常:", str(e))
return False
def emergency_reduce_live(ratio=0.5):
try:
for p in (exchange.GetPositions(SYMBOL) or []):
if p['Amount']<=0:
continue
exchange.CreateOrder(SYMBOL,'closebuy' if p['Type']==0 else 'closesell',-1,p['Amount']*ratio)
Log("🚨 实盘紧急减仓 {:.0f}%".format(ratio*100))
except Exception as e:
Log("⚠️ 实盘紧急减仓异常:", str(e))
def _append_log(entry):
tlog = json.loads(_G('pt_trade_log') or '[]')
tlog.append(entry)
if len(tlog)>100:
tlog=tlog[-100:]
_G('pt_trade_log',json.dumps(tlog))
# ================================================================
# 七、风险控制
# ================================================================
def check_sl_tp(price):
try:
sl = float(STOP_LOSS_PCT)
tp = float(TAKE_PROFIT_PCT)
except:
sl = 0.06
tp = 0.18
mode = _G('rp_trade_mode') or TRADE_MODE
if mode=='live':
try:
pos = exchange.GetPositions(SYMBOL)
if not pos:
return None
p = pos[0]
entry = float(p.get('Price',0))
if entry<=0:
return None
chg = (price-entry)/entry if p['Type']==0 else (entry-price)/entry
except:
return None
else:
pos = json.loads(_G('pt_position') or 'null')
if not pos:
return None
entry = pos['entry_price']
chg = (price-entry)/entry if pos['side']=='long' else (entry-price)/entry
if chg<=-sl:
return 'sl'
if chg>=tp:
return 'tp'
return None
def check_emergency(price):
last = _G('rp_last_check_price')
if last is None:
_G('rp_last_check_price',price)
return
last = float(last)
mode = _G('rp_trade_mode') or TRADE_MODE
pos = json.loads(_G('pt_position') or 'null')
if mode=='live':
try:
positions = exchange.GetPositions(SYMBOL)
pos = positions[0] if positions else None
if not pos:
_G('rp_last_check_price',price)
return
side='long' if pos['Type']==0 else 'short'
except:
return
else:
if not pos:
_G('rp_last_check_price',price)
return
side = pos['side']
drop = (last-price)/last if side=='long' else (price-last)/last
if drop>=EMERGENCY_DROP:
cnt = (_G('rp_emergency_count') or 0) + 1
_G('rp_emergency_count',cnt)
Log("🚨 紧急风控[{}] 不利{:.2f}% 第{}次".format(side,drop*100,cnt))
if mode=='live':
emergency_reduce_live(EMERGENCY_REDUCE)
else:
emergency_reduce_paper(price,side,EMERGENCY_REDUCE)
else:
_G('rp_last_check_price',price)
def get_current_side():
mode = _G('rp_trade_mode') or TRADE_MODE
if mode=='live':
try:
pos = exchange.GetPositions(SYMBOL)
if not pos:
return 'none'
for p in pos:
if p['Amount']>0:
return 'long' if p['Type']==0 else 'short'
except:
pass
return 'none'
pos = json.loads(_G('pt_position') or 'null')
return pos['side'] if pos else 'none'
# ================================================================
# 八、自适应轮询 & 1分钟持仓刷新
# ================================================================
def get_adaptive_sleep(rv30_val):
try:
av = float(rv30_val) if rv30_val else 0
except:
av = 0
if av>0.50:
Log("⚠️ 高波动{:.1f}%→15min".format(av*100))
return FAST_INTERVAL
if av>0.30:
Log("📊 中波动{:.1f}%→30min".format(av*100))
return MID_INTERVAL
Log("✅ 低波动{:.1f}%→60min".format(av*100))
return SLOW_INTERVAL
def sleep_with_pnl_refresh(total_ms, chart, init_cap):
elapsed = 0
while elapsed<total_ms:
step = min(PNL_INTERVAL,total_ms-elapsed)
Sleep(step)
elapsed += step
try:
ticker = exchange.GetTicker(SYMBOL)
if not ticker:
continue
price = float(ticker['Last'])
equity = calc_equity(price)
chart.add(0,[int(time.time()*1000),equity])
LogProfit(equity-float(init_cap),'&')
pos = json.loads(_G('pt_position') or 'null')
if pos:
unreal = ((price-pos['entry_price'])*pos['qty'] if pos['side']=='long'
else (pos['entry_price']-price)*pos['qty'])
LogStatus("💓 1min刷新 价:{:.4f} [{}]浮盈:${:.4f} 权益:${:.2f} {}".format(
price,pos['side'],unreal,equity,_D()))
except Exception as e:
Log("刷新异常:",str(e))
# ================================================================
# 九、主策略决策
# ================================================================
def run_once(mode, price, consensus):
sig = consensus['trust_adjusted']
try:
lt = float(LONG_THRESH)
st = float(SHORT_THRESH)
ct = float(CLOSE_THRESH)
except:
lt = 0.05
st = -0.05
ct = 0.02
sltp = check_sl_tp(price)
if sltp:
Log("触发{} 价:{}".format('止损🚨' if sltp=='sl' else '止盈✅',price))
(live_close(sltp) if mode=='live' else paper_close(price,sltp))
return
side = get_current_side()
if side=='none':
if sig>lt:
(live_open('long', price) if mode=='live' else paper_open('long', price, sig))
elif sig<st:
(live_open('short', price) if mode=='live' else paper_open('short', price, sig))
else:
Log("⚪ 中性{:.4f},观望".format(sig))
elif side=='long':
if abs(sig)<ct or sig<st:
Log("📉 平多 sig={:.4f}".format(sig))
(live_close('signal_reverse') if mode=='live' else paper_close(price,'signal_reverse'))
else:
Log("✋ 持多 sig={:.4f}".format(sig))
elif side=='short':
if abs(sig)<ct or sig>lt:
Log("📈 平空 sig={:.4f}".format(sig))
(live_close('signal_reverse') if mode=='live' else paper_close(price,'signal_reverse'))
else:
Log("✋ 持空 sig={:.4f}".format(sig))
# ================================================================
# 十、仪表板
# ================================================================
def render_dashboard(consensus, price, equity, init_cap, run_cnt, sleep_ms):
mode = _G('rp_trade_mode') or TRADE_MODE
mtag = '🔴 实盘' if mode=='live' else '🟢 模拟'
cash = safe_get_cash()
realized = float(_G('pt_realized_pnl') or 0)
pnl = equity-float(init_cap)
pct = pnl/float(init_cap)*100 if init_cap else 0
emgc = _G('rp_emergency_count') or 0
ivdesc = {FAST_INTERVAL:'15min高波',MID_INTERVAL:'30min中波',SLOW_INTERVAL:'60min低波'}.get(sleep_ms,'?')
sig = consensus['trust_adjusted']
lt = float(LONG_THRESH or 0.05)
st = float(SHORT_THRESH or -0.05)
stag = ('🟢 BULLISH({:.4f})'.format(sig) if sig>lt else
'🔴 BEARISH({:.4f})'.format(sig) if sig<st else
'⚪ NEUTRAL({:.4f})'.format(sig))
pos = json.loads(_G('pt_position') or 'null')
if pos:
unreal = ((price-pos['entry_price'])*pos['qty'] if pos['side']=='long'
else (pos['entry_price']-price)*pos['qty'])
pdesc = "[{}]qty:{:.6f}@{:.4f} 浮盈:${:.4f}".format(pos['side'],pos['qty'],pos['entry_price'],unreal)
else:
pdesc = "无持仓"
t1 = {'type':'table','title':'📊 账户 ({}) | {}'.format(mtag,_D()),
'cols':['模式','权益','盈亏','收益率','现金','已实现','紧急','次数','轮询','操作'],
'rows':[[mtag,'${:.2f}'.format(equity),
('+$' if pnl>=0 else'-$')+'{:.2f}'.format(abs(pnl)),
('+' if pct>=0 else'')+'{:.2f}%'.format(pct),
'${:.2f}'.format(cash),('+$' if realized>=0 else'-$')+'{:.2f}'.format(abs(realized)),
'🚨{}次'.format(emgc),'🔄{}次'.format(run_cnt),'⏱'+ivdesc,
[{'type':'button','cmd':'切换模式','name':'切换模式'},
{'type':'button','cmd':'全部平仓','name':'📤全平'},
{'type':'button','cmd':'重置模拟','name':'🔄重置'},
{'type':'button','cmd':'立即再平衡','name':'🔁立即再平衡'}]]]}
t2 = {'type':'table','title':'🧠 99人KOL共识',
'cols':['信号','Trust-Adj','IC加权','等权均值','看多','看空','中性','持仓'],
'rows':[[stag,'{:.4f}'.format(consensus['trust_adjusted']),
'{:.4f}'.format(consensus['ic_weighted']),
'{:.4f}'.format(consensus.get('equal_mean',0)),
str(consensus['long_count']),str(consensus['short_count']),
str(consensus['neutral_count']),pdesc]]}
sc_rows = []
for sc,d in sorted(consensus.get('by_school',{}).items(), key=lambda x:x[1]['count'], reverse=True):
m = d.get('mean_signal',0)
sc_rows.append([sc,str(d['count']),
('🟢' if m>0.03 else '🔴' if m<-0.03 else '⚪')+' {:.3f}'.format(m),
str(d['long']),str(d['short']),str(d['neutral'])])
if not sc_rows:
sc_rows=[['暂无','-','-','-','-','-']]
t3 = {'type':'table','title':'🏫 By School 学派分组',
'cols':['学派','人数','均值','多','空','中'],'rows':sc_rows}
frows = [[fid,('🟢 ' if v>0 else '🔴 ')+'{:.3f}'.format(v)]
for fid,v in consensus['top_factors']]
if not frows:
frows=[['无激活因子','-']]
t4 = {'type':'table','title':'⚡ 激活因子 Top10',
'cols':['因子ID','得分'],'rows':frows}
tprows = []
for t in consensus.get('top_traders',[]):
sv = t['signal']
arr='🟢' if sv>0.1 else '🔴' if sv<-0.1 else '⚪'
tprows.append(['@'+t['handle'][:20],'{:+.2f}'.format(t['ic']),
arr+' {:+.3f}'.format(sv),t.get('school','?')[:12]])
if not tprows:
tprows=[['暂无','-','-','-']]
t5 = {'type':'table','title':'🌟 Top15高IC交易员',
'cols':['交易员','IC','信号','学派'],'rows':tprows}
rvrows = []
for t in consensus.get('reverse_traders',[]):
sv = t['signal']
fl = -sv
arr='🟢' if fl>0.1 else '🔴' if fl<-0.1 else '⚪'
rvrows.append(['@'+t['handle'][:20],'{:+.2f}'.format(t['ic']),
'{:+.3f}'.format(sv),arr+' {:+.3f}'.format(fl),t.get('school','?')[:12]])
if not rvrows:
rvrows=[['暂无','-','-','-','-']]
t6 = {'type':'table','title':'🔄 Top10反向指标交易员',
'cols':['交易员','IC','原始','翻转','学派'],'rows':rvrows}
tlog = json.loads(_G('pt_trade_log') or '[]')
trrows = [[('🟢多' if t.get('side')=='long' else '🔴空'),t.get('action','-'),
'{:.4f}'.format(t.get('close',t.get('price',t.get('entry',0)))),
('${:.4f}'.format(t.get('pnl',0)) if 'pnl' in t else '-'),
str(t.get('reason',t.get('time','-')))[-16:]]
for t in reversed(tlog[-8:])]
if not trrows:
trrows=[['暂无','-','-','-','-']]
t7 = {'type':'table','title':'📜 近期成交',
'cols':['方向','动作','价格','PnL','原因/时间'],'rows':trrows}
LogStatus(
'{} | {} | {} | ⏱{} | v4.0\n'.format(mtag,_D(),stag,ivdesc)+
'`'+json.dumps(t1)+'`\n`'+json.dumps(t2)+'`\n`'+json.dumps(t3)+'`\n'+
'`'+json.dumps(t4)+'`\n`'+json.dumps(t5)+'`\n`'+json.dumps(t6)+'`\n'+
'`'+json.dumps(t7)+'`')
# ================================================================
# 十一、命令处理
# ================================================================
def handle_command(price):
cmd = GetCommand()
if not cmd:
return
mode = _G('rp_trade_mode') or TRADE_MODE
if cmd=='切换模式':
nxt = 'live' if mode=='paper' else 'paper'
_G('rp_trade_mode',nxt)
Log('🔄 模式切换:',mode,'→',nxt)
elif cmd=='全部平仓':
(live_close('manual') if mode=='live' else paper_close(price,'manual'))
elif cmd=='重置模拟':
try:
ic = float(INIT_CAPITAL)
except:
ic = 10000.0
_G('pt_cash',ic)
_G('pt_position',json.dumps(None))
_G('pt_trade_log',json.dumps([]))
_G('pt_realized_pnl',0.0)
_G('rp_emergency_count',0)
_G('rp_last_check_price',None)
global _profiles_cache, _ic_cache
_profiles_cache = None
_ic_cache = None
Log('🔄 重置完成,保留本地缓存,下次优先读取缓存')
elif cmd=='立即再平衡':
_G('rp_force_rebal',True)
Log('🔁 已标记立即再平衡')
# ================================================================
# 十二、初始化
# ================================================================
def init_state():
if _G('kol_initialized'):
return
_G('kol_initialized',True)
_G('rp_trade_mode',TRADE_MODE)
try:
ic = float(INIT_CAPITAL)
except:
ic = 10000.0
_G('pt_cash',ic)
_G('pt_init_capital',ic)
_G('pt_position',json.dumps(None))
_G('pt_trade_log',json.dumps([]))
_G('pt_realized_pnl',0.0)
_G('kol_run_count',0)
_G('rp_emergency_count',0)
_G('rp_last_check_price',None)
_G('rp_last_rebal_time',0)
_G('rp_force_rebal',False)
Log('='*62)
Log(' KOL蒸馏共识策略 v4.0 — 默认优先本地缓存')
Log(' 数据源: https://github.com/0xquqi/crypto-kol-quant')
Log(' 模式:', '🔴 实盘' if TRADE_MODE=='live' else '🟢 模拟')
Log(' 合约:', SYMBOL, '| 初始资金:', ic, 'USDT')
Log(' 缓存目录:', CACHE_DIR)
Log('='*62)
# ================================================================
# 十三、主循环
# ================================================================
def main():
init_state()
try:
init_cap = float(INIT_CAPITAL)
except:
init_cap = 10000.0
chart = Chart({'title':{'text':'KOL蒸馏共识策略 v4.0 · 净值曲线'},
'xAxis':{'type':'datetime'},
'yAxis':{'title':{'text':'权益(USDT)'}},
'series':[{'name':'账户权益','type':'line','data':[]}]})
load_profiles()
load_ic()
sleep_ms = SLOW_INTERVAL
macro = {}
last_macro = 0
last_rebal = float(_G('rp_last_rebal_time') or 0)
while True:
try:
ticker = exchange.GetTicker(SYMBOL)
if not ticker:
Log("⚠️ 无行情")
Sleep(30000)
continue
price = float(ticker['Last'])
handle_command(price)
run_cnt = (_G('kol_run_count') or 0) + 1
_G('kol_run_count',run_cnt)
mode = _G('rp_trade_mode') or TRADE_MODE
check_emergency(price)
now_ts = int(time.time())
if now_ts-last_macro>6*3600:
Log("── 刷新宏观 ──")
macro = fetch_macro(days=int(MACRO_LOOKBACK_DAYS))
last_macro = now_ts
try:
lb = int(LOOKBACK_BARS)
except:
lb = 600
records = exchange.GetRecords(SYMBOL,PERIOD_D1)
if not records or len(records)<50:
Log("⚠️ K线不足")
Sleep(60000)
continue
records = records[-lb:]
feat_df = build_features(records, macro if macro else None)
factor_scores = evaluate_factors(feat_df)
active = sum(1 for v in factor_scores.values() if abs(v)>0.05)
Log("因子:{} 激活:{} 个".format(len(factor_scores),active))
consensus = compute_consensus(factor_scores)
Log("共识 trust={:.4f} ic={:.4f} eq={:.4f} L{}/S{}/N{} 学派{}个".format(
consensus['trust_adjusted'],consensus['ic_weighted'],
consensus.get('equal_mean',0),consensus['long_count'],
consensus['short_count'],consensus['neutral_count'],
len(consensus.get('by_school',{}))))
rv30_val = float(feat_df['rv30'].iloc[-1]) if 'rv30' in feat_df.columns else 0.3
sleep_ms = get_adaptive_sleep(rv30_val)
try:
rh = float(REBALANCE_HOURS)
except:
rh = 24.0
force = _G('rp_force_rebal') or False
time_due = (now_ts-float(last_rebal)) > rh*3600
if force or time_due:
Log("═══ 决策 {} ═══".format('强制' if force else '定时{:.0f}h'.format(rh)))
run_once(mode,price,consensus)
last_rebal = now_ts
_G('rp_last_rebal_time',now_ts)
_G('rp_force_rebal',False)
else:
run_once(mode,price,consensus)
equity = calc_equity(price)
if mode=='live':
try:
acc = exchange.GetAccount()
equity = float(acc['Balance'])+float(acc['FrozenBalance']) if acc else init_cap
except:
pass
chart.add(0,[int(time.time()*1000),equity])
LogProfit(equity-init_cap,'&')
render_dashboard(consensus,price,equity,init_cap,run_cnt,sleep_ms)
except Exception as e:
Log("🔥 主循环异常:",str(e))
sleep_with_pnl_refresh(sleep_ms,chart,init_cap)