initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
299
external_factors/esoteric_factors_service.py
Executable file
299
external_factors/esoteric_factors_service.py
Executable file
@@ -0,0 +1,299 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
import zoneinfo
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import numpy as np
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MarketIndicators:
|
||||
"""
|
||||
Mathematical and astronomical calculations for the Esoteric Factors mapping.
|
||||
Evaluates completely locally without external API dependencies.
|
||||
"""
|
||||
def __init__(self):
|
||||
# Regions defined by NON-OVERLAPPING population clusters for accurate global weighting.
|
||||
# Population in Millions (approximate). Liquidity weight is estimated crypto volume share.
|
||||
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}
|
||||
]
|
||||
|
||||
# Market cycle: Bitcoin halving based, ~4 years
|
||||
self.cycle_length_days = 1460
|
||||
self.last_halving = datetime.datetime(2024, 4, 20, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Cache for expensive ASTRO calculations
|
||||
self._cache = {
|
||||
'moon': {'val': None, 'ts': 0},
|
||||
'mercury': {'val': None, 'ts': 0}
|
||||
}
|
||||
self.cache_ttl_seconds = 3600 * 6 # Update astro every 6 hours
|
||||
|
||||
def get_calendar_items(self, now: datetime.datetime) -> Dict[str, int]:
|
||||
return {
|
||||
'year': now.year,
|
||||
'month': now.month,
|
||||
'day_of_month': now.day,
|
||||
'hour': now.hour,
|
||||
'minute': now.minute,
|
||||
'day_of_week': now.weekday(), # 0=Monday
|
||||
'week_of_year': now.isocalendar().week
|
||||
}
|
||||
|
||||
def is_tradfi_open(self, region_name: str, local_time: datetime.datetime) -> bool:
|
||||
day = local_time.weekday()
|
||||
if day >= 5: return False
|
||||
hour_dec = local_time.hour + local_time.minute / 60.0
|
||||
|
||||
if 'Americas' in region_name:
|
||||
return 9.5 <= hour_dec < 16.0
|
||||
elif 'EMEA' in region_name:
|
||||
return 8.0 <= hour_dec < 16.5
|
||||
elif 'Asia' in region_name:
|
||||
return 9.0 <= hour_dec < 15.0
|
||||
return False
|
||||
|
||||
def get_regional_times(self, now_utc: datetime.datetime) -> Dict[str, Any]:
|
||||
times = {}
|
||||
for region in self.regions:
|
||||
tz = zoneinfo.ZoneInfo(region['tz'])
|
||||
local_time = now_utc.astimezone(tz)
|
||||
times[region['name']] = {
|
||||
'hour': local_time.hour + local_time.minute / 60.0,
|
||||
'is_tradfi_open': self.is_tradfi_open(region['name'], local_time)
|
||||
}
|
||||
return times
|
||||
|
||||
def get_liquidity_session(self, now_utc: datetime.datetime) -> str:
|
||||
utc_hour = now_utc.hour + now_utc.minute / 60.0
|
||||
if 13 <= utc_hour < 17:
|
||||
return "LONDON_NEW_YORK_OVERLAP"
|
||||
elif 8 <= utc_hour < 13:
|
||||
return "LONDON_MORNING"
|
||||
elif 0 <= utc_hour < 8:
|
||||
return "ASIA_PACIFIC"
|
||||
elif 17 <= utc_hour < 21:
|
||||
return "NEW_YORK_AFTERNOON"
|
||||
else:
|
||||
return "LOW_LIQUIDITY"
|
||||
|
||||
def get_weighted_times(self, now_utc: datetime.datetime) -> tuple[float, float]:
|
||||
pop_sin, pop_cos = 0.0, 0.0
|
||||
liq_sin, liq_cos = 0.0, 0.0
|
||||
|
||||
total_pop = sum(r['pop'] for r in self.regions)
|
||||
|
||||
for region in self.regions:
|
||||
tz = zoneinfo.ZoneInfo(region['tz'])
|
||||
local_time = now_utc.astimezone(tz)
|
||||
hour_frac = (local_time.hour + local_time.minute / 60.0) / 24.0
|
||||
angle = 2 * math.pi * hour_frac
|
||||
|
||||
w_pop = region['pop'] / total_pop
|
||||
pop_sin += math.sin(angle) * w_pop
|
||||
pop_cos += math.cos(angle) * w_pop
|
||||
|
||||
w_liq = region['liq_weight']
|
||||
liq_sin += math.sin(angle) * w_liq
|
||||
liq_cos += math.cos(angle) * w_liq
|
||||
|
||||
pop_angle = math.atan2(pop_sin, pop_cos)
|
||||
if pop_angle < 0: pop_angle += 2 * math.pi
|
||||
pop_hour = (pop_angle / (2 * math.pi)) * 24
|
||||
|
||||
liq_angle = math.atan2(liq_sin, liq_cos)
|
||||
if liq_angle < 0: liq_angle += 2 * math.pi
|
||||
liq_hour = (liq_angle / (2 * math.pi)) * 24
|
||||
|
||||
return round(pop_hour, 2), round(liq_hour, 2)
|
||||
|
||||
def get_market_cycle_position(self, now_utc: datetime.datetime) -> float:
|
||||
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_fibonacci_time(self, now_utc: datetime.datetime) -> Dict[str, Any]:
|
||||
mins_passed = now_utc.hour * 60 + now_utc.minute
|
||||
fib_seq = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]
|
||||
closest = min(fib_seq, key=lambda x: abs(x - mins_passed))
|
||||
distance = abs(mins_passed - closest)
|
||||
strength = 1.0 - min(distance / 30.0, 1.0)
|
||||
return {'closest_fib_minute': closest, 'harmonic_strength': round(strength, 3)}
|
||||
|
||||
def get_moon_phase(self, now_utc: datetime.datetime) -> Dict[str, Any]:
|
||||
now_ts = now_utc.timestamp()
|
||||
if self._cache['moon']['val'] and (now_ts - self._cache['moon']['ts'] < self.cache_ttl_seconds):
|
||||
return self._cache['moon']['val']
|
||||
|
||||
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"
|
||||
|
||||
result = {'illumination': float(illumination), 'phase_name': phase_name}
|
||||
self._cache['moon'] = {'val': result, 'ts': now_ts}
|
||||
return result
|
||||
|
||||
def is_mercury_retrograde(self, now_utc: datetime.datetime) -> bool:
|
||||
now_ts = now_utc.timestamp()
|
||||
if self._cache['mercury']['val'] is not None and (now_ts - self._cache['mercury']['ts'] < self.cache_ttl_seconds):
|
||||
return self._cache['mercury']['val']
|
||||
|
||||
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.transform_to('geocentrictrueecliptic').lon.deg
|
||||
lon_later = merc_later.transform_to('geocentrictrueecliptic').lon.deg
|
||||
|
||||
diff = (lon_later - lon_now) % 360
|
||||
is_retro = diff > 180
|
||||
except Exception as e:
|
||||
logger.error(f"Astro calc error: {e}")
|
||||
|
||||
self._cache['mercury'] = {'val': is_retro, 'ts': now_ts}
|
||||
return is_retro
|
||||
|
||||
def get_indicators(self, custom_now: Optional[datetime.datetime] = None) -> Dict[str, Any]:
|
||||
"""Generate full suite of Esoteric Matrix factors."""
|
||||
now_utc = custom_now if custom_now else datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
pop_hour, liq_hour = self.get_weighted_times(now_utc)
|
||||
moon_data = self.get_moon_phase(now_utc)
|
||||
calendar = self.get_calendar_items(now_utc)
|
||||
|
||||
return {
|
||||
'timestamp': now_utc.isoformat(),
|
||||
'unix': int(now_utc.timestamp()),
|
||||
'calendar': calendar,
|
||||
'fibonacci_time': self.get_fibonacci_time(now_utc),
|
||||
'regional_times': self.get_regional_times(now_utc),
|
||||
'population_weighted_hour': pop_hour,
|
||||
'liquidity_weighted_hour': liq_hour,
|
||||
'liquidity_session': self.get_liquidity_session(now_utc),
|
||||
'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)),
|
||||
}
|
||||
|
||||
|
||||
class EsotericFactorsService:
|
||||
"""
|
||||
Continuous evaluation service for Esoteric Factors.
|
||||
Dumps state deterministically to be consumed by the live trading orchestrator/Forewarning layers.
|
||||
"""
|
||||
def __init__(self, output_dir: str = "", poll_interval_s: float = 60.0):
|
||||
# Default to same structure as external factors
|
||||
if not output_dir:
|
||||
self.output_dir = Path(__file__).parent / "eso_cache"
|
||||
else:
|
||||
self.output_dir = Path(output_dir)
|
||||
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.poll_interval_s = poll_interval_s
|
||||
self.engine = MarketIndicators()
|
||||
|
||||
self._latest_data = {}
|
||||
self._running = False
|
||||
self._task = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
async def _update_loop(self):
|
||||
logger.info(f"EsotericFactorsService starting. Polling every {self.poll_interval_s}s.")
|
||||
while self._running:
|
||||
try:
|
||||
# 1. Compute Matrix
|
||||
data = self.engine.get_indicators()
|
||||
|
||||
# 2. Store in memory
|
||||
with self._lock:
|
||||
self._latest_data = data
|
||||
|
||||
# 3. Dump purely to fast JSON
|
||||
self._write_to_disk(data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Esoteric update loop: {e}", exc_info=True)
|
||||
|
||||
await asyncio.sleep(self.poll_interval_s)
|
||||
|
||||
def _write_to_disk(self, data: dict):
|
||||
# Fast write pattern via atomic tmp rename strategy
|
||||
target_path = self.output_dir / "latest_esoteric_factors.json"
|
||||
tmp_path = self.output_dir / "latest_esoteric_factors.tmp"
|
||||
|
||||
try:
|
||||
with open(tmp_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
tmp_path.replace(target_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write Esoteric factors to disk: {e}")
|
||||
|
||||
def get_latest(self) -> dict:
|
||||
"""Non-blocking sub-millisecond retrieval of the latest internal state."""
|
||||
with self._lock:
|
||||
return self._latest_data.copy()
|
||||
|
||||
def start(self):
|
||||
"""Starts the background calculation loop (Threaded/Async wrapper)."""
|
||||
if self._running: return
|
||||
self._running = True
|
||||
|
||||
def run_async():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self._update_loop())
|
||||
|
||||
self._thread = threading.Thread(target=run_async, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
if hasattr(self, '_thread'):
|
||||
self._thread.join(timeout=2.0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
|
||||
svc = EsotericFactorsService(poll_interval_s=5.0)
|
||||
print("Starting Esoteric Factors Service test run for 15 seconds...")
|
||||
svc.start()
|
||||
|
||||
for _ in range(3):
|
||||
time.sleep(5)
|
||||
latest = svc.get_latest()
|
||||
print(f"Update: Moon Illumination={latest.get('moon_illumination'):.3f} | Liquid Session={latest.get('liquidity_session')} | PopHour={latest.get('population_weighted_hour')}")
|
||||
|
||||
svc.stop()
|
||||
print("Stopped successfully.")
|
||||
Reference in New Issue
Block a user