Make signal personalities to run in paralel.

This commit is contained in:
Kalzu Rekku
2026-01-18 14:11:33 +02:00
parent 44cc5da36f
commit 12b22f2dae

283
signals/signals.py Executable file → Normal file
View File

@@ -1,7 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
BTCUSDT Signal Generator BTCUSDT Signal Generator - Multi-Personality Edition
Generates trading signals from candles.db and analysis.db Generates trading signals from candles.db and analysis.db
Runs both scalping and swing personalities simultaneously
Streams signals via Unix Domain Socket Streams signals via Unix Domain Socket
""" """
@@ -46,11 +47,10 @@ class Config:
self.log_file = data.get("log_file", "logs/signal_generator.log") self.log_file = data.get("log_file", "logs/signal_generator.log")
self.log_to_stdout = data.get("log_to_stdout", True) self.log_to_stdout = data.get("log_to_stdout", True)
self.poll_interval = data.get("poll_interval", 0.5) self.poll_interval = data.get("poll_interval", 0.5)
self.personality = data.get("personality", "scalping")
self.timeframes = data.get("timeframes", ["1m", "5m"]) self.timeframes = data.get("timeframes", ["1m", "5m"])
self.lookback = data.get("lookback", 200) self.lookback = data.get("lookback", 200)
# Signal thresholds # Signal thresholds (can be personality-specific)
self.min_confidence = data.get("min_confidence", 0.6) self.min_confidence = data.get("min_confidence", 0.6)
self.cooldown_seconds = data.get("cooldown_seconds", 60) self.cooldown_seconds = data.get("cooldown_seconds", 60)
@@ -92,13 +92,14 @@ def setup_logging(config: Config):
return logging.getLogger(__name__) return logging.getLogger(__name__)
# Signal Generator Class # Signal Generator Class (per-personality)
class SignalGenerator: class PersonalityEngine:
def __init__(self, config: Config, logger: logging.Logger): """Single personality signal generation engine"""
def __init__(self, personality: str, config: Config, logger: logging.Logger):
self.personality = personality
self.config = config self.config = config
self.logger = logger self.logger = logger
self.running = False
self.debug_mode = False
self.last_signal_time = {} self.last_signal_time = {}
self.signal_history = deque(maxlen=100) self.signal_history = deque(maxlen=100)
self.stats = { self.stats = {
@@ -106,20 +107,9 @@ class SignalGenerator:
"buy_signals": 0, "buy_signals": 0,
"sell_signals": 0, "sell_signals": 0,
"last_signal_time": None, "last_signal_time": None,
"uptime_start": datetime.now(timezone.utc),
"errors": 0, "errors": 0,
"config_reloads": 0,
} }
self.lock = threading.Lock()
# Unix socket
self.socket = None
self.connections = []
# Health check socket
self.health_socket = None
# Control socket
self.control_socket = None
def fetch_and_enrich(self, timeframe: str) -> Optional[pd.DataFrame]: def fetch_and_enrich(self, timeframe: str) -> Optional[pd.DataFrame]:
"""Fetch data from databases and enrich with additional indicators""" """Fetch data from databases and enrich with additional indicators"""
@@ -181,7 +171,7 @@ class SignalGenerator:
df = df[df["timestamp"] < (current_time - window)] df = df[df["timestamp"] < (current_time - window)]
if len(df) < 50: if len(df) < 50:
self.logger.debug(f"Not enough data for {timeframe}: {len(df)} rows") self.logger.debug(f"[{self.personality}] Not enough data for {timeframe}: {len(df)} rows")
return None return None
# Drop rows with NULL in critical columns # Drop rows with NULL in critical columns
@@ -189,7 +179,7 @@ class SignalGenerator:
if len(df) < 50: if len(df) < 50:
self.logger.debug( self.logger.debug(
f"Not enough valid data after NULL filtering for {timeframe}" f"[{self.personality}] Not enough valid data after NULL filtering for {timeframe}"
) )
return None return None
@@ -210,7 +200,8 @@ class SignalGenerator:
return df return df
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching data for {timeframe}: {e}") self.logger.error(f"[{self.personality}] Error fetching data for {timeframe}: {e}")
with self.lock:
self.stats["errors"] += 1 self.stats["errors"] += 1
return None return None
@@ -219,13 +210,13 @@ class SignalGenerator:
) -> Optional[Dict]: ) -> Optional[Dict]:
"""Generate signal using scalping personality""" """Generate signal using scalping personality"""
if len(df) < 21: if len(df) < 21:
self.logger.debug(f"[{timeframe}] Insufficient data: {len(df)} rows") self.logger.debug(f"[{self.personality}/{timeframe}] Insufficient data: {len(df)} rows")
return None return None
latest = df.iloc[-1] latest = df.iloc[-1]
prev = df.iloc[-2] prev = df.iloc[-2]
# Check for NULL indicators - skip if essential indicators are missing # Check for NULL indicators
required_cols = [ required_cols = [
"ema_9", "ema_9",
"ema_21", "ema_21",
@@ -236,7 +227,7 @@ class SignalGenerator:
"macd_signal", "macd_signal",
] ]
if any(pd.isna(latest[col]) for col in required_cols): if any(pd.isna(latest[col]) for col in required_cols):
self.logger.debug(f"[{timeframe}] Skipping: missing required indicators") self.logger.debug(f"[{self.personality}/{timeframe}] Skipping: missing required indicators")
return None return None
score = 0 score = 0
@@ -262,12 +253,6 @@ class SignalGenerator:
reasons.append("EMA9 crossed below EMA21") reasons.append("EMA9 crossed below EMA21")
signal_type = "SELL" signal_type = "SELL"
# Log EMA status for debugging
self.logger.debug(
f"[{timeframe}] EMA9={latest['ema_9']:.2f} vs EMA21={latest['ema_21']:.2f}, "
f"Prev: EMA9={prev['ema_9']:.2f} vs EMA21={prev['ema_21']:.2f}"
)
# Stochastic # Stochastic
if signal_type == "BUY": if signal_type == "BUY":
if latest["stoch_k"] > latest["stoch_d"] and latest["stoch_k"] < 30: if latest["stoch_k"] > latest["stoch_d"] and latest["stoch_k"] < 30:
@@ -299,13 +284,6 @@ class SignalGenerator:
score += weights["macd"] score += weights["macd"]
reasons.append("MACD bearish") reasons.append("MACD bearish")
# Debug output
if signal_type:
self.logger.debug(
f"[{timeframe}] Potential {signal_type} signal - Score: {score:.3f} "
f"(threshold: {self.config.min_confidence}), Reasons: {len(reasons)}"
)
if signal_type and score >= self.config.min_confidence: if signal_type and score >= self.config.min_confidence:
return { return {
"signal": signal_type, "signal": signal_type,
@@ -327,7 +305,7 @@ class SignalGenerator:
latest = df.iloc[-1] latest = df.iloc[-1]
prev = df.iloc[-2] prev = df.iloc[-2]
# Check for NULL indicators - skip if essential indicators are missing # Check for NULL indicators
required_cols = [ required_cols = [
"sma_50", "sma_50",
"sma_200", "sma_200",
@@ -339,7 +317,7 @@ class SignalGenerator:
"buy_ratio", "buy_ratio",
] ]
if any(pd.isna(latest[col]) for col in required_cols): if any(pd.isna(latest[col]) for col in required_cols):
self.logger.debug(f"Skipping {timeframe}: missing required indicators") self.logger.debug(f"[{self.personality}/{timeframe}] Skipping: missing required indicators")
return None return None
score = 0 score = 0
@@ -418,7 +396,7 @@ class SignalGenerator:
def generate_signal(self, timeframe: str) -> Optional[Dict]: def generate_signal(self, timeframe: str) -> Optional[Dict]:
"""Main signal generation dispatcher""" """Main signal generation dispatcher"""
# Check cooldown # Check cooldown
cooldown_key = f"{self.config.personality}_{timeframe}" cooldown_key = f"{self.personality}_{timeframe}"
if cooldown_key in self.last_signal_time: if cooldown_key in self.last_signal_time:
elapsed = time.time() - self.last_signal_time[cooldown_key] elapsed = time.time() - self.last_signal_time[cooldown_key]
if elapsed < self.config.cooldown_seconds: if elapsed < self.config.cooldown_seconds:
@@ -428,12 +406,12 @@ class SignalGenerator:
if df is None: if df is None:
return None return None
if self.config.personality == "scalping": if self.personality == "scalping":
signal = self.generate_signal_scalping(df, timeframe) signal = self.generate_signal_scalping(df, timeframe)
elif self.config.personality == "swing": elif self.personality == "swing":
signal = self.generate_signal_swing(df, timeframe) signal = self.generate_signal_swing(df, timeframe)
else: else:
self.logger.error(f"Unknown personality: {self.config.personality}") self.logger.error(f"Unknown personality: {self.personality}")
return None return None
if signal: if signal:
@@ -441,28 +419,64 @@ class SignalGenerator:
signal["generated_at"] = datetime.now(timezone.utc).isoformat() signal["generated_at"] = datetime.now(timezone.utc).isoformat()
# Update stats # Update stats
with self.lock:
self.stats["total_signals"] += 1 self.stats["total_signals"] += 1
if signal["signal"] == "BUY": if signal["signal"] == "BUY":
self.stats["buy_signals"] += 1 self.stats["buy_signals"] += 1
else: else:
self.stats["sell_signals"] += 1 self.stats["sell_signals"] += 1
self.stats["last_signal_time"] = signal["generated_at"] self.stats["last_signal_time"] = signal["generated_at"]
self.signal_history.append(signal) self.signal_history.append(signal)
return signal return signal
# Main Signal Generator Coordinator
class SignalGenerator:
def __init__(self, config: Config, logger: logging.Logger):
self.config = config
self.logger = logger
self.running = False
self.debug_mode = False
# Create personality engines
self.engines = {
"scalping": PersonalityEngine("scalping", config, logger),
"swing": PersonalityEngine("swing", config, logger),
}
self.global_stats = {
"uptime_start": datetime.now(timezone.utc),
"config_reloads": 0,
}
# Unix socket
self.socket = None
self.connections = []
self.connections_lock = threading.Lock()
# Health check socket
self.health_socket = None
# Control socket
self.control_socket = None
# Thread pool
self.threads = []
def broadcast_signal(self, signal: Dict): def broadcast_signal(self, signal: Dict):
"""Broadcast signal to all connected clients""" """Broadcast signal to all connected clients"""
message = json.dumps(signal) + "\n" message = json.dumps(signal) + "\n"
message_bytes = message.encode("utf-8") message_bytes = message.encode("utf-8")
with self.connections_lock:
disconnected = [] disconnected = []
for conn in self.connections: for conn in self.connections:
try: try:
conn.sendall(message_bytes) conn.sendall(message_bytes)
self.logger.info( self.logger.info(
f"Sent {signal['signal']} signal: {signal['timeframe']} @ {signal['price']} (conf: {signal['confidence']})" f"[{signal['personality']}] Sent {signal['signal']} signal: "
f"{signal['timeframe']} @ {signal['price']} (conf: {signal['confidence']})"
) )
except Exception as e: except Exception as e:
self.logger.warning(f"Failed to send to client: {e}") self.logger.warning(f"Failed to send to client: {e}")
@@ -485,7 +499,7 @@ class SignalGenerator:
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.bind(self.config.socket_path) self.socket.bind(self.config.socket_path)
self.socket.listen(5) self.socket.listen(5)
self.socket.settimeout(0.1) # Non-blocking accept self.socket.settimeout(0.1)
self.logger.info(f"Signal socket listening on {self.config.socket_path}") self.logger.info(f"Signal socket listening on {self.config.socket_path}")
@@ -545,12 +559,10 @@ class SignalGenerator:
) )
cursor = conn_c.cursor() cursor = conn_c.cursor()
# Check total rows
cursor.execute("SELECT COUNT(*) FROM candles") cursor.execute("SELECT COUNT(*) FROM candles")
status["candles_db"]["row_count"] = cursor.fetchone()[0] status["candles_db"]["row_count"] = cursor.fetchone()[0]
status["candles_db"]["accessible"] = True status["candles_db"]["accessible"] = True
# Check per-timeframe data
for tf in self.config.timeframes: for tf in self.config.timeframes:
cursor.execute( cursor.execute(
"SELECT COUNT(*), MAX(timestamp) FROM candles WHERE timeframe = ?", "SELECT COUNT(*), MAX(timestamp) FROM candles WHERE timeframe = ?",
@@ -578,7 +590,6 @@ class SignalGenerator:
status["analysis_db"]["row_count"] = cursor.fetchone()[0] status["analysis_db"]["row_count"] = cursor.fetchone()[0]
status["analysis_db"]["accessible"] = True status["analysis_db"]["accessible"] = True
# Check per-timeframe data
for tf in self.config.timeframes: for tf in self.config.timeframes:
cursor.execute( cursor.execute(
"SELECT COUNT(*), MAX(timestamp) FROM analysis WHERE timeframe = ?", "SELECT COUNT(*), MAX(timestamp) FROM analysis WHERE timeframe = ?",
@@ -602,28 +613,56 @@ class SignalGenerator:
try: try:
conn, _ = self.health_socket.accept() conn, _ = self.health_socket.accept()
uptime = datetime.now(timezone.utc) - self.stats["uptime_start"] uptime = datetime.now(timezone.utc) - self.global_stats["uptime_start"]
db_status = self.check_database_status() db_status = self.check_database_status()
# Aggregate stats from both engines
total_stats = {
"total_signals": 0,
"buy_signals": 0,
"sell_signals": 0,
"errors": 0,
}
personality_stats = {}
recent_signals = []
for name, engine in self.engines.items():
with engine.lock:
personality_stats[name] = {
"total_signals": engine.stats["total_signals"],
"buy_signals": engine.stats["buy_signals"],
"sell_signals": engine.stats["sell_signals"],
"last_signal": engine.stats["last_signal_time"],
"errors": engine.stats["errors"],
"recent_signals": list(engine.signal_history)[-5:],
}
total_stats["total_signals"] += engine.stats["total_signals"]
total_stats["buy_signals"] += engine.stats["buy_signals"]
total_stats["sell_signals"] += engine.stats["sell_signals"]
total_stats["errors"] += engine.stats["errors"]
recent_signals.extend(list(engine.signal_history)[-5:])
# Sort recent signals by timestamp
recent_signals.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
health = { health = {
"status": "running", "status": "running",
"personality": self.config.personality, "mode": "multi-personality",
"personalities": ["scalping", "swing"],
"timeframes": self.config.timeframes, "timeframes": self.config.timeframes,
"uptime_seconds": int(uptime.total_seconds()), "uptime_seconds": int(uptime.total_seconds()),
"total_signals": self.stats["total_signals"], "total_stats": total_stats,
"buy_signals": self.stats["buy_signals"], "personality_stats": personality_stats,
"sell_signals": self.stats["sell_signals"],
"last_signal": self.stats["last_signal_time"],
"errors": self.stats["errors"],
"connected_clients": len(self.connections), "connected_clients": len(self.connections),
"recent_signals": list(self.signal_history)[-5:], "recent_signals": recent_signals[:10],
"databases": db_status, "databases": db_status,
"config": { "config": {
"min_confidence": self.config.min_confidence, "min_confidence": self.config.min_confidence,
"cooldown_seconds": self.config.cooldown_seconds, "cooldown_seconds": self.config.cooldown_seconds,
"lookback": self.config.lookback, "lookback": self.config.lookback,
"weights": self.config.weights[self.config.personality], "weights": self.config.weights,
"reloads": self.stats["config_reloads"], "reloads": self.global_stats["config_reloads"],
}, },
"debug_mode": self.debug_mode, "debug_mode": self.debug_mode,
} }
@@ -641,7 +680,6 @@ class SignalGenerator:
try: try:
conn, _ = self.control_socket.accept() conn, _ = self.control_socket.accept()
# Receive command
data = conn.recv(4096).decode("utf-8").strip() data = conn.recv(4096).decode("utf-8").strip()
if not data: if not data:
@@ -668,25 +706,17 @@ class SignalGenerator:
if action == "reload": if action == "reload":
try: try:
old_personality = self.config.personality
old_confidence = self.config.min_confidence old_confidence = self.config.min_confidence
self.config.reload() self.config.reload()
self.stats["config_reloads"] += 1 self.global_stats["config_reloads"] += 1
self.logger.info( self.logger.info(f"Config reloaded: min_confidence={self.config.min_confidence}")
f"Config reloaded: personality={self.config.personality}, "
f"min_confidence={self.config.min_confidence}"
)
return { return {
"status": "success", "status": "success",
"message": "Configuration reloaded", "message": "Configuration reloaded",
"changes": { "changes": {
"personality": {
"old": old_personality,
"new": self.config.personality,
},
"min_confidence": { "min_confidence": {
"old": old_confidence, "old": old_confidence,
"new": self.config.min_confidence, "new": self.config.min_confidence,
@@ -696,21 +726,6 @@ class SignalGenerator:
except Exception as e: except Exception as e:
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}
elif action == "set_personality":
personality = cmd.get("value")
if personality in ["scalping", "swing"]:
self.config.personality = personality
self.logger.info(f"Personality changed to: {personality}")
return {
"status": "success",
"message": f"Personality set to {personality}",
}
else:
return {
"status": "error",
"message": "Invalid personality (use 'scalping' or 'swing')",
}
elif action == "set_confidence": elif action == "set_confidence":
try: try:
confidence = float(cmd.get("value")) confidence = float(cmd.get("value"))
@@ -755,21 +770,24 @@ class SignalGenerator:
} }
elif action == "clear_cooldowns": elif action == "clear_cooldowns":
self.last_signal_time.clear() for engine in self.engines.values():
self.logger.info("Signal cooldowns cleared") engine.last_signal_time.clear()
self.logger.info("All signal cooldowns cleared")
return {"status": "success", "message": "All cooldowns cleared"} return {"status": "success", "message": "All cooldowns cleared"}
elif action == "reset_stats": elif action == "reset_stats":
self.stats = { for engine in self.engines.values():
with engine.lock:
engine.stats = {
"total_signals": 0, "total_signals": 0,
"buy_signals": 0, "buy_signals": 0,
"sell_signals": 0, "sell_signals": 0,
"last_signal_time": None, "last_signal_time": None,
"uptime_start": datetime.now(timezone.utc),
"errors": 0, "errors": 0,
"config_reloads": self.stats["config_reloads"],
} }
self.signal_history.clear() engine.signal_history.clear()
self.global_stats["uptime_start"] = datetime.now(timezone.utc)
self.logger.info("Statistics reset") self.logger.info("Statistics reset")
return {"status": "success", "message": "Statistics reset"} return {"status": "success", "message": "Statistics reset"}
@@ -780,6 +798,7 @@ class SignalGenerator:
"""Accept new client connections""" """Accept new client connections"""
try: try:
conn, _ = self.socket.accept() conn, _ = self.socket.accept()
with self.connections_lock:
self.connections.append(conn) self.connections.append(conn)
self.logger.info( self.logger.info(
f"New client connected. Total clients: {len(self.connections)}" f"New client connected. Total clients: {len(self.connections)}"
@@ -789,6 +808,33 @@ class SignalGenerator:
except Exception as e: except Exception as e:
self.logger.debug(f"Accept error: {e}") self.logger.debug(f"Accept error: {e}")
def personality_worker(self, personality: str):
"""Worker thread for a specific personality"""
engine = self.engines[personality]
self.logger.info(f"[{personality}] Worker thread started")
while self.running:
try:
for timeframe in self.config.timeframes:
try:
signal = engine.generate_signal(timeframe)
if signal:
self.broadcast_signal(signal)
except Exception as e:
self.logger.error(f"[{personality}] Error processing {timeframe}: {e}")
with engine.lock:
engine.stats["errors"] += 1
time.sleep(self.config.poll_interval)
except Exception as e:
self.logger.error(f"[{personality}] Worker error: {e}")
with engine.lock:
engine.stats["errors"] += 1
time.sleep(1) # Brief pause on error
self.logger.info(f"[{personality}] Worker thread stopped")
def run(self): def run(self):
"""Main processing loop""" """Main processing loop"""
self.running = True self.running = True
@@ -796,34 +842,29 @@ class SignalGenerator:
self.setup_health_socket() self.setup_health_socket()
self.setup_control_socket() self.setup_control_socket()
self.logger.info( self.logger.info("Multi-personality signal generator started")
f"Signal generator started - Personality: {self.config.personality}" self.logger.info(f"Running personalities: scalping, swing")
)
self.logger.info(f"Monitoring timeframes: {', '.join(self.config.timeframes)}") self.logger.info(f"Monitoring timeframes: {', '.join(self.config.timeframes)}")
self.logger.info(f"Poll interval: {self.config.poll_interval}s") self.logger.info(f"Poll interval: {self.config.poll_interval}s")
# Start personality worker threads
for personality in ["scalping", "swing"]:
thread = threading.Thread(
target=self.personality_worker,
args=(personality,),
name=f"{personality}-worker",
daemon=True
)
thread.start()
self.threads.append(thread)
try: try:
# Main thread handles connections and management
while self.running: while self.running:
# Accept new connections
self.accept_connections() self.accept_connections()
# Handle health checks
self.handle_health_checks() self.handle_health_checks()
# Handle control commands
self.handle_control_commands() self.handle_control_commands()
time.sleep(0.1)
# Generate signals for each timeframe
for timeframe in self.config.timeframes:
try:
signal = self.generate_signal(timeframe)
if signal:
self.broadcast_signal(signal)
except Exception as e:
self.logger.error(f"Error processing {timeframe}: {e}")
self.stats["errors"] += 1
time.sleep(self.config.poll_interval)
except KeyboardInterrupt: except KeyboardInterrupt:
self.logger.info("Received interrupt signal") self.logger.info("Received interrupt signal")
@@ -833,7 +874,15 @@ class SignalGenerator:
def cleanup(self): def cleanup(self):
"""Cleanup resources""" """Cleanup resources"""
self.logger.info("Shutting down...") self.logger.info("Shutting down...")
self.running = False
# Wait for worker threads
self.logger.info("Waiting for worker threads to finish...")
for thread in self.threads:
thread.join(timeout=2.0)
# Close connections
with self.connections_lock:
for conn in self.connections: for conn in self.connections:
try: try:
conn.close() conn.close()
@@ -864,17 +913,17 @@ def main():
generator = SignalGenerator(config, logger) generator = SignalGenerator(config, logger)
# Signal handlers for hot-reload and control # Signal handlers
def reload_config(sig, frame): def reload_config(sig, frame):
"""SIGUSR1: Reload configuration""" """SIGUSR1: Reload configuration"""
logger.info("Received SIGUSR1 - Reloading configuration...") logger.info("Received SIGUSR1 - Reloading configuration...")
try: try:
old_personality = config.personality old_confidence = config.min_confidence
config.reload() config.reload()
generator.stats["config_reloads"] += 1 generator.global_stats["config_reloads"] += 1
logger.info( logger.info(
f"Configuration reloaded successfully " f"Configuration reloaded successfully "
f"(personality: {old_personality} -> {config.personality})" f"(min_confidence: {old_confidence} -> {config.min_confidence})"
) )
except Exception as e: except Exception as e:
logger.error(f"Failed to reload configuration: {e}") logger.error(f"Failed to reload configuration: {e}")