Make signal personalities to run in paralel.
This commit is contained in:
283
signals/signals.py
Executable file → Normal file
283
signals/signals.py
Executable file → Normal 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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user