188 lines
7.2 KiB
Python
188 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Config Monitoring Script: Alerts on suboptimal quote snapshot intervals
|
|
Purpose: Detect overtrading risk when QUOTE_SNAPSHOT_INTERVAL_SECONDS < 30s
|
|
Author: momentum-system team
|
|
"""
|
|
|
|
import os
|
|
import yaml
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
# Discord webhook/notification endpoint (configure as needed)
|
|
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL", "")
|
|
PROJECT_CHANNEL_ID = os.getenv("MOMENTUM_PROJECT_CHANNEL", "1537839209954541658")
|
|
|
|
# Optimal ranges
|
|
MIN_SNAPSHOT_INTERVAL_SECONDS = 30 # Below this triggers alert
|
|
MAX_SNAPSHOT_INTERVAL_SECONDS = 120 # Above this may miss signals
|
|
|
|
|
|
def parse_env_file(env_path: str) -> dict:
|
|
"""Parse .env or Docker Compose environment section."""
|
|
config = {}
|
|
|
|
if not os.path.exists(env_path):
|
|
return config
|
|
|
|
with open(env_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#'):
|
|
if '=' in line:
|
|
key, _, value = line.partition('=')
|
|
config[key.strip()] = value.strip()
|
|
|
|
return config
|
|
|
|
|
|
def load_docker_compose(compose_path: str) -> dict:
|
|
"""Load Docker Compose file and extract environment variables."""
|
|
if not os.path.exists(compose_path):
|
|
return {}
|
|
|
|
with open(compose_path, 'r') as f:
|
|
try:
|
|
return yaml.safe_load(f)
|
|
except yaml.YAMLError:
|
|
return {}
|
|
|
|
|
|
def check_snapshot_interval(env_config: dict) -> tuple[bool, float]:
|
|
"""
|
|
Check if snapshot interval is within optimal range.
|
|
Returns: (is_valid, current_value)
|
|
"""
|
|
interval_str = env_config.get("QUOTE_SNAPSHOT_INTERVAL_SECONDS", "60")
|
|
|
|
try:
|
|
interval = int(interval_str)
|
|
is_valid = MIN_SNAPSHOT_INTERVAL_SECONDS <= interval <= MAX_SNAPSHOT_INTERVAL_SECONDS
|
|
return is_valid, interval
|
|
except (ValueError, TypeError):
|
|
return False, float('nan')
|
|
|
|
|
|
def main():
|
|
"""Main monitoring logic."""
|
|
|
|
# Check multiple possible config locations
|
|
config_paths = [
|
|
"/opt/data/workspace/momentum-system/.env",
|
|
"/mnt/Volume/Storage/Apps/momentum/compose.yaml",
|
|
"/etc/docker-compose.yml",
|
|
]
|
|
|
|
env_config = {}
|
|
found_valid_config = False
|
|
|
|
for config_path in config_paths:
|
|
if os.path.exists(config_path):
|
|
found_valid_config = True
|
|
print(f"Checking config at: {config_path}")
|
|
|
|
try:
|
|
# Check both .env file format and Docker Compose YAML
|
|
env_config = parse_env_file(config_path)
|
|
|
|
if "QUOTE_SNAPSHOT_INTERVAL_SECONDS" not in env_config:
|
|
# Try to extract from yaml content if it's a compose file
|
|
compose_data = load_docker_compose(config_path)
|
|
if compose_data and 'services' in compose_data:
|
|
for service_name in ["dashboard", "loop"]:
|
|
service = compose_data['services'].get(service_name, {})
|
|
service_env = service.get('environment', {})
|
|
|
|
# Extract value from environment string like 'VAR=value'
|
|
interval_value = None
|
|
for env_var in service_env:
|
|
if env_var.startswith("QUOTE_SNAPSHOT_INTERVAL_SECONDS="):
|
|
parts = env_var.split('=', 1)
|
|
if len(parts) == 2:
|
|
interval_value = parts[1].strip().strip("'\"")
|
|
break
|
|
|
|
if interval_value:
|
|
env_config["QUOTE_SNAPSHOT_INTERVAL_SECONDS"] = interval_value
|
|
|
|
except Exception as e:
|
|
print(f"Error parsing config: {e}")
|
|
continue
|
|
|
|
if not found_valid_config:
|
|
print("No configuration file found at expected paths")
|
|
# Check via Docker compose volumes
|
|
try:
|
|
import subprocess
|
|
result = subprocess.run(
|
|
["docker", "compose", "config"],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd="/mnt/Volume/Storage/Apps/momentum"
|
|
)
|
|
if result.returncode == 0:
|
|
env_config = {
|
|
line.strip().split('=', 1): val.strip()
|
|
for line in result.stdout.split('\n')
|
|
if '=' in line and not line.startswith('#')
|
|
}
|
|
except Exception as e:
|
|
print(f"Could not load via docker compose config: {e}")
|
|
env_config = {}
|
|
|
|
# Check snapshot interval
|
|
if not env_config or "QUOTE_SNAPSHOT_INTERVAL_SECONDS" not in env_config:
|
|
print("QUOTE_SNAPSHOT_INTERVAL_SECONDS not found in configuration")
|
|
return
|
|
|
|
is_valid, interval = check_snapshot_interval(env_config)
|
|
|
|
current_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
if is_valid:
|
|
print(f"✅ OK ({current_time}): QUOTE_SNAPSHOT_INTERVAL_SECONDS={interval}s")
|
|
print(f" Current interval is within optimal range [{MIN_SNAPSHOT_INTERVAL_SECONDS}-{MAX_SNAPSHOT_INTERVAL_SECONDS}s]")
|
|
|
|
# Log to audit file
|
|
log_path = Path("/opt/data/workspace/momentum-system/logs/config_audit.log")
|
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(log_path, 'a') as f:
|
|
f.write(f"{current_time}|STATUS=OK|VALUE={interval}s\n")
|
|
else:
|
|
print(f"🚨 ALERT ({current_time}): QUOTE_SNAPSHOT_INTERVAL_SECONDS={interval}s")
|
|
print(f" Current interval is below minimum of {MIN_SNAPSHOT_INTERVAL_SECONDS}s")
|
|
print(f" Risk: OVERTRADING during volatile opens!")
|
|
|
|
# Log to audit file
|
|
log_path = Path("/opt/data/workspace/momentum-system/logs/config_audit.log")
|
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(log_path, 'a') as f:
|
|
f.write(f"{current_time}|STATUS=ALERT|VALUE={interval}s|MIN={MIN_SNAPSHOT_INTERVAL_SECONDS}s|MAX={MAX_SNAPSHOT_INTERVAL_SECONDS}s\n")
|
|
|
|
# Send Discord notification if webhook configured
|
|
if DISCORD_WEBHOOK_URL and interval < MIN_SNAPSHOT_INTERVAL_SECONDS:
|
|
try:
|
|
import requests
|
|
message = (
|
|
f"🚨 **Momentum Bot Config Alert**\n\n"
|
|
f"⏱️ Snapshot Interval Too Fast\n\n"
|
|
f":warning: Current: `{interval}s`\n"
|
|
f":white_check_mark: Required: `>= {MIN_SNAPSHOT_INTERVAL_SECONDS}s`\n\n"
|
|
f"🔗 View Config:\n`/mnt/Volume/Storage/Apps/momentum/compose.yaml`\n\n"
|
|
f"💡 Impact:\n- Overtrading during volatile opens\n- Phantom signals from noise\n- Poor entry timing\n"
|
|
)
|
|
|
|
requests.post(DISCORD_WEBHOOK_URL, json={"content": message})
|
|
print(f"\n📢 Discord notification sent")
|
|
except Exception as e:
|
|
print(f"Could not send Discord notification: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|