313 lines
13 KiB
Python
313 lines
13 KiB
Python
|
|
import sys, time, datetime, zoneinfo, json, math
|
||
|
|
from pathlib import Path
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
|
|
||
|
|
from astropy.time import Time
|
||
|
|
import astropy.coordinates as coord
|
||
|
|
import astropy.units as u
|
||
|
|
from astropy.coordinates import solar_system_ephemeris, get_body, EarthLocation
|
||
|
|
|
||
|
|
from nautilus_dolphin.nautilus.alpha_orchestrator import NDAlphaEngine
|
||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
||
|
|
from nautilus_dolphin.nautilus.ob_features import OBFeatureEngine
|
||
|
|
from nautilus_dolphin.nautilus.ob_provider import MockOBProvider
|
||
|
|
|
||
|
|
class MarketIndicators:
|
||
|
|
def __init__(self):
|
||
|
|
self.regions = [
|
||
|
|
{'name': 'Americas', 'tz': 'America/New_York', 'pop': 1000, 'liq_weight': 0.35},
|
||
|
|
{'name': 'EMEA', 'tz': 'Europe/London', 'pop': 2200, 'liq_weight': 0.30},
|
||
|
|
{'name': 'South_Asia', 'tz': 'Asia/Kolkata', 'pop': 1400, 'liq_weight': 0.05},
|
||
|
|
{'name': 'East_Asia', 'tz': 'Asia/Shanghai', 'pop': 1600, 'liq_weight': 0.20},
|
||
|
|
{'name': 'Oceania_SEA', 'tz': 'Asia/Singapore', 'pop': 800, 'liq_weight': 0.10}
|
||
|
|
]
|
||
|
|
self.cycle_length_days = 1460
|
||
|
|
self.last_halving = datetime.datetime(2024, 4, 20, tzinfo=datetime.timezone.utc)
|
||
|
|
|
||
|
|
def get_calendar_items(self, now):
|
||
|
|
return {
|
||
|
|
'year': now.year,
|
||
|
|
'month': now.month,
|
||
|
|
'day_of_month': now.day,
|
||
|
|
'hour': now.hour,
|
||
|
|
'minute': now.minute,
|
||
|
|
'day_of_week': now.weekday(),
|
||
|
|
'week_of_year': now.isocalendar().week
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_market_cycle_position(self, now_utc):
|
||
|
|
days_since_halving = (now_utc - self.last_halving).days
|
||
|
|
position = (days_since_halving % self.cycle_length_days) / self.cycle_length_days
|
||
|
|
return position
|
||
|
|
|
||
|
|
def get_moon_phase(self, now_utc):
|
||
|
|
t = Time(now_utc)
|
||
|
|
with solar_system_ephemeris.set('builtin'):
|
||
|
|
moon = get_body('moon', t)
|
||
|
|
sun = get_body('sun', t)
|
||
|
|
elongation = sun.separation(moon)
|
||
|
|
phase_angle = np.arctan2(sun.distance * np.sin(elongation),
|
||
|
|
moon.distance - sun.distance * np.cos(elongation))
|
||
|
|
illumination = (1 + np.cos(phase_angle)) / 2.0
|
||
|
|
|
||
|
|
phase_name = "WAXING"
|
||
|
|
if illumination < 0.03: phase_name = "NEW_MOON"
|
||
|
|
elif illumination > 0.97: phase_name = "FULL_MOON"
|
||
|
|
elif illumination < 0.5: phase_name = "WAXING_CRESCENT" if moon.dec.deg > sun.dec.deg else "WANING_CRESCENT"
|
||
|
|
else: phase_name = "WAXING_GIBBOUS" if moon.dec.deg > sun.dec.deg else "WANING_GIBBOUS"
|
||
|
|
|
||
|
|
return {'illumination': float(illumination), 'phase_name': phase_name}
|
||
|
|
|
||
|
|
def is_mercury_retrograde(self, now_utc):
|
||
|
|
t = Time(now_utc)
|
||
|
|
is_retro = False
|
||
|
|
try:
|
||
|
|
with solar_system_ephemeris.set('builtin'):
|
||
|
|
loc = EarthLocation.of_site('greenwich')
|
||
|
|
merc_now = get_body('mercury', t, loc)
|
||
|
|
merc_later = get_body('mercury', t + 1 * u.day, loc)
|
||
|
|
|
||
|
|
lon_now = merc_now.geometrictrueecliptic.lon.deg
|
||
|
|
lon_later = merc_later.geometrictrueecliptic.lon.deg
|
||
|
|
|
||
|
|
diff = (lon_later - lon_now) % 360
|
||
|
|
is_retro = diff > 180
|
||
|
|
except Exception as e:
|
||
|
|
pass
|
||
|
|
return is_retro
|
||
|
|
|
||
|
|
def get_indicators(self, now_utc):
|
||
|
|
# We drop hour-specific ones since we evaluate at a fixed time daily
|
||
|
|
moon_data = self.get_moon_phase(now_utc)
|
||
|
|
calendar = self.get_calendar_items(now_utc)
|
||
|
|
return {
|
||
|
|
'timestamp': now_utc.isoformat(),
|
||
|
|
'day_of_week': calendar['day_of_week'],
|
||
|
|
'week_of_year': calendar['week_of_year'],
|
||
|
|
'market_cycle_position': round(self.get_market_cycle_position(now_utc), 4),
|
||
|
|
'moon_illumination': moon_data['illumination'],
|
||
|
|
'moon_phase_name': moon_data['phase_name'],
|
||
|
|
'mercury_retrograde': int(self.is_mercury_retrograde(now_utc)),
|
||
|
|
}
|
||
|
|
|
||
|
|
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
|
||
|
|
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
|
||
|
|
parquet_files = [p for p in parquet_files if 'catalog' not in str(p)]
|
||
|
|
|
||
|
|
print("Loading data & extracting daily precursor AND ESOTERIC metrics...")
|
||
|
|
|
||
|
|
daily_metrics = []
|
||
|
|
mi = MarketIndicators()
|
||
|
|
|
||
|
|
for pf in parquet_files:
|
||
|
|
ds = pf.stem
|
||
|
|
df = pd.read_parquet(pf)
|
||
|
|
|
||
|
|
# 1. Structural Precursors
|
||
|
|
vd = df['vel_div'].fillna(0).values
|
||
|
|
vol_accel = np.diff(vd, prepend=vd[0])
|
||
|
|
daily_vol_accel_max = np.max(np.abs(vol_accel))
|
||
|
|
|
||
|
|
assets = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT']
|
||
|
|
valid_assets = [a for a in assets if a in df.columns]
|
||
|
|
if len(valid_assets) > 1:
|
||
|
|
rets = df[valid_assets].pct_change().fillna(0)
|
||
|
|
corr_matrix = rets.corr().values
|
||
|
|
cross_corr = corr_matrix[np.triu_indices_from(corr_matrix, k=1)]
|
||
|
|
mean_cross_corr = np.nanmean(cross_corr)
|
||
|
|
max_cross_corr = np.nanmax(cross_corr)
|
||
|
|
else:
|
||
|
|
mean_cross_corr = 0; max_cross_corr = 0
|
||
|
|
|
||
|
|
entropy_max = df['instability_50'].max() if 'instability_50' in df.columns else 0
|
||
|
|
v750_max = df['v750_lambda_max_velocity'].max() if 'v750_lambda_max_velocity' in df.columns else 0
|
||
|
|
v50_max = df['v50_lambda_max_velocity'].max() if 'v50_lambda_max_velocity' in df.columns else 0
|
||
|
|
|
||
|
|
# 2. Esoteric Factors
|
||
|
|
y, m, d = map(int, ds.split('-'))
|
||
|
|
dt_utc = datetime.datetime(y, m, d, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||
|
|
eso = mi.get_indicators(dt_utc)
|
||
|
|
|
||
|
|
# Optionally save to disk as requested
|
||
|
|
eso_path = VBT_DIR / f"ESOTERIC_data_{ds}.json"
|
||
|
|
if not eso_path.exists():
|
||
|
|
with open(eso_path, 'w') as f:
|
||
|
|
json.dump(eso, f, indent=2)
|
||
|
|
|
||
|
|
daily_metrics.append({
|
||
|
|
'Date': ds,
|
||
|
|
'vol_accel_max': daily_vol_accel_max,
|
||
|
|
'cross_corr_mean': mean_cross_corr,
|
||
|
|
'cross_corr_max': max_cross_corr,
|
||
|
|
'entropy_max': entropy_max,
|
||
|
|
'v750_max': v750_max,
|
||
|
|
'v50_max': v50_max,
|
||
|
|
'day_of_week': eso['day_of_week'],
|
||
|
|
'week_of_year': eso['week_of_year'],
|
||
|
|
'market_cycle_position': eso['market_cycle_position'],
|
||
|
|
'moon_illumination': eso['moon_illumination'],
|
||
|
|
'mercury_retrograde': eso['mercury_retrograde']
|
||
|
|
})
|
||
|
|
|
||
|
|
metrics_df = pd.DataFrame(daily_metrics).set_index('Date')
|
||
|
|
precursor_df = metrics_df.shift(1).dropna() # Shift 1 day to ensure it's a T-1 precursor
|
||
|
|
|
||
|
|
print("Running fast 6.0x trajectory to isolate daily PnL...")
|
||
|
|
|
||
|
|
pq_data = {}
|
||
|
|
all_vols = []
|
||
|
|
for pf in parquet_files:
|
||
|
|
df = pd.read_parquet(pf)
|
||
|
|
ac = [c for c in df.columns if c not in {'timestamp', 'scan_number', 'v50_lambda_max_velocity', 'v150_lambda_max_velocity',
|
||
|
|
'v300_lambda_max_velocity', 'v750_lambda_max_velocity', 'vel_div',
|
||
|
|
'instability_50', 'instability_150'}]
|
||
|
|
dv = df['vel_div'].values if 'vel_div' in df.columns else np.zeros(len(df))
|
||
|
|
pq_data[pf.stem] = (df, ac, dv)
|
||
|
|
|
||
|
|
if 'BTCUSDT' in df.columns:
|
||
|
|
pr = df['BTCUSDT'].values
|
||
|
|
for i in range(60, len(pr)):
|
||
|
|
seg = pr[max(0,i-50):i]
|
||
|
|
if len(seg)<10: continue
|
||
|
|
v = float(np.std(np.diff(seg)/seg[:-1]))
|
||
|
|
if v > 0: all_vols.append(v)
|
||
|
|
|
||
|
|
vol_p60 = float(np.percentile(all_vols, 60))
|
||
|
|
|
||
|
|
acb = AdaptiveCircuitBreaker()
|
||
|
|
acb.preload_w750([pf.stem for pf in parquet_files])
|
||
|
|
|
||
|
|
mock = MockOBProvider(imbalance_bias=-0.09, depth_scale=1.0,
|
||
|
|
assets=["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"],
|
||
|
|
imbalance_biases={"BNBUSDT": 0.20, "SOLUSDT": 0.20})
|
||
|
|
ob_engine_inst = OBFeatureEngine(mock)
|
||
|
|
ob_engine_inst.preload_date("mock", mock.get_assets())
|
||
|
|
|
||
|
|
ENGINE_KWARGS = dict(
|
||
|
|
initial_capital=25000.0, vel_div_threshold=-0.02, vel_div_extreme=-0.05,
|
||
|
|
min_leverage=0.5, max_leverage=6.0, leverage_convexity=3.0,
|
||
|
|
fraction=0.20, fixed_tp_pct=0.0099, stop_pct=1.0, max_hold_bars=120,
|
||
|
|
use_direction_confirm=True, dc_lookback_bars=7, dc_min_magnitude_bps=0.75,
|
||
|
|
dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5,
|
||
|
|
use_asset_selection=True, min_irp_alignment=0.45,
|
||
|
|
use_sp_fees=True, use_sp_slippage=True,
|
||
|
|
use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40,
|
||
|
|
lookback=100, use_alpha_layers=True, use_dynamic_leverage=True, seed=42,
|
||
|
|
)
|
||
|
|
|
||
|
|
engine = NDAlphaEngine(**ENGINE_KWARGS)
|
||
|
|
engine.set_ob_engine(ob_engine_inst)
|
||
|
|
|
||
|
|
daily_returns = {}
|
||
|
|
bar_idx = 0
|
||
|
|
all_vols_engine = []
|
||
|
|
|
||
|
|
for pf in parquet_files:
|
||
|
|
ds = pf.stem
|
||
|
|
cs = engine.capital
|
||
|
|
|
||
|
|
acb_info = acb.get_dynamic_boost_for_date(ds, ob_engine=ob_engine_inst)
|
||
|
|
base_boost = acb_info['boost']
|
||
|
|
beta = acb_info['beta']
|
||
|
|
|
||
|
|
df, acols, dvol_raw = pq_data[ds]
|
||
|
|
ph = {}
|
||
|
|
|
||
|
|
for ri in range(len(df)):
|
||
|
|
row = df.iloc[ri]
|
||
|
|
vd = dvol_raw[ri]
|
||
|
|
if not np.isfinite(vd): bar_idx+=1; continue
|
||
|
|
|
||
|
|
prices = {}
|
||
|
|
for ac in acols:
|
||
|
|
p = row[ac]
|
||
|
|
if p and p > 0 and np.isfinite(p):
|
||
|
|
prices[ac] = float(p)
|
||
|
|
if ac not in ph: ph[ac] = []
|
||
|
|
ph[ac].append(float(p))
|
||
|
|
if len(ph[ac]) > 500: ph[ac] = ph[ac][-200:]
|
||
|
|
if not prices: bar_idx+=1; continue
|
||
|
|
|
||
|
|
btc_hist = ph.get("BTCUSDT", [])
|
||
|
|
engine_vrok = False
|
||
|
|
if len(btc_hist) >= 50:
|
||
|
|
seg = btc_hist[-50:]
|
||
|
|
vd_eng = float(np.std(np.diff(seg)/np.array(seg[:-1])))
|
||
|
|
all_vols_engine.append(vd_eng)
|
||
|
|
if len(all_vols_engine) > 100:
|
||
|
|
engine_vrok = vd_eng > np.percentile(all_vols_engine, 60)
|
||
|
|
|
||
|
|
if beta > 0:
|
||
|
|
ss = 0.0
|
||
|
|
if vd < -0.02:
|
||
|
|
raw = (-0.02 - float(vd)) / (-0.02 - -0.05)
|
||
|
|
ss = min(1.0, max(0.0, raw)) ** 3.0
|
||
|
|
engine.regime_size_mult = base_boost * (1.0 + beta * ss)
|
||
|
|
else:
|
||
|
|
engine.regime_size_mult = base_boost
|
||
|
|
|
||
|
|
engine.process_bar(bar_idx=bar_idx, vel_div=float(vd), prices=prices, vol_regime_ok=engine_vrok, price_histories=ph)
|
||
|
|
bar_idx += 1
|
||
|
|
|
||
|
|
daily_returns[ds] = (engine.capital - cs) / cs if cs > 0 else 0
|
||
|
|
|
||
|
|
returns_df = pd.DataFrame.from_dict(daily_returns, orient='index', columns=['Return'])
|
||
|
|
merged = precursor_df.join(returns_df, how='inner')
|
||
|
|
|
||
|
|
threshold_pnl = merged['Return'].quantile(0.10)
|
||
|
|
merged['Is_Tail'] = merged['Return'] <= threshold_pnl
|
||
|
|
base_p_tail = merged['Is_Tail'].mean()
|
||
|
|
|
||
|
|
print(f"\n==================================================================================================")
|
||
|
|
print(f" CONDITIONAL PROBABILITY ANALYSIS: P(Tail | Precursor Spike)")
|
||
|
|
print(f" Baseline P(Tail) = {base_p_tail:.1%}")
|
||
|
|
print(f"==================================================================================================")
|
||
|
|
print(f"{'Feature':<25} | {'P(Tail|X > 75th)':<20} | {'P(Tail|X > 90th)':<20} | {'P(Tail|X > 95th)':<20}")
|
||
|
|
print("-" * 98)
|
||
|
|
|
||
|
|
features = ['vol_accel_max', 'v750_max', 'v50_max', 'entropy_max', 'cross_corr_max']
|
||
|
|
|
||
|
|
for f in features:
|
||
|
|
p75 = merged[f].quantile(0.75)
|
||
|
|
p90 = merged[f].quantile(0.90)
|
||
|
|
p95 = merged[f].quantile(0.95)
|
||
|
|
|
||
|
|
cond_75 = merged[merged[f] > p75]['Is_Tail'].mean() if len(merged[merged[f]>p75]) > 0 else 0
|
||
|
|
cond_90 = merged[merged[f] > p90]['Is_Tail'].mean() if len(merged[merged[f]>p90]) > 0 else 0
|
||
|
|
cond_95 = merged[merged[f] > p95]['Is_Tail'].mean() if len(merged[merged[f]>p95]) > 0 else 0
|
||
|
|
|
||
|
|
print(f"{f:<25} | {cond_75:>19.1%} | {cond_90:>19.1%} | {cond_95:>19.1%}")
|
||
|
|
|
||
|
|
print(f"\n==================================================================================================")
|
||
|
|
print(f" ESOTERIC FACTORS CORRELATION WITH EXTREME LEFT TAILS")
|
||
|
|
print(f"==================================================================================================")
|
||
|
|
|
||
|
|
esoteric_features = ['day_of_week', 'moon_illumination', 'market_cycle_position', 'mercury_retrograde']
|
||
|
|
for f in esoteric_features:
|
||
|
|
# See if Esoteric factors map to higher chance of tail events
|
||
|
|
if f == 'day_of_week':
|
||
|
|
print("\nDay of Week P(Tail):")
|
||
|
|
for d in range(7):
|
||
|
|
subset = merged[merged[f] == d]
|
||
|
|
if len(subset) > 0:
|
||
|
|
print(f" Day {d}: {subset['Is_Tail'].mean():.1%} (N={len(subset)})")
|
||
|
|
elif f == 'mercury_retrograde':
|
||
|
|
print("\nMercury Retrograde P(Tail):")
|
||
|
|
for d in [0, 1]:
|
||
|
|
subset = merged[merged[f] == d]
|
||
|
|
if len(subset) > 0:
|
||
|
|
print(f" Retrograde={d}: {subset['Is_Tail'].mean():.1%} (N={len(subset)})")
|
||
|
|
else:
|
||
|
|
# Continuous values
|
||
|
|
p75 = merged[f].quantile(0.75)
|
||
|
|
cond_75 = merged[merged[f] > p75]['Is_Tail'].mean() if len(merged[merged[f]>p75]) > 0 else 0
|
||
|
|
p25 = merged[f].quantile(0.25)
|
||
|
|
cond_25 = merged[merged[f] < p25]['Is_Tail'].mean() if len(merged[merged[f]<p25]) > 0 else 0
|
||
|
|
print(f"\n{f}:")
|
||
|
|
print(f" P(Tail | Top 25% {f}): {cond_75:.1%}")
|
||
|
|
print(f" P(Tail | Bot 25% {f}): {cond_25:.1%}")
|
||
|
|
|