fixes, old way to handle strategies
This commit is contained in:
@ -5,8 +5,12 @@ import os
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
import sqlite3
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
from logging_utils import setup_logging
|
||||
from hyperliquid.info import Info
|
||||
from hyperliquid.utils import constants
|
||||
|
||||
class BaseStrategy(ABC):
|
||||
"""
|
||||
@ -14,20 +18,23 @@ class BaseStrategy(ABC):
|
||||
It provides common functionality like loading data, saving status, and state management.
|
||||
"""
|
||||
|
||||
def __init__(self, strategy_name: str, params: dict):
|
||||
# Note: log_level is not needed here as logging is set up by the process
|
||||
def __init__(self, strategy_name: str, params: dict, trade_signal_queue: multiprocessing.Queue = None, shared_status: dict = None):
|
||||
self.strategy_name = strategy_name
|
||||
self.params = params
|
||||
self.trade_signal_queue = trade_signal_queue
|
||||
# Optional multiprocessing.Manager().dict() to hold live status (avoids file IO)
|
||||
self.shared_status = shared_status
|
||||
|
||||
self.coin = params.get("coin", "N/A")
|
||||
self.timeframe = params.get("timeframe", "N/A")
|
||||
self.db_path = os.path.join("_data", "market_data.db")
|
||||
self.status_file_path = os.path.join("_data", f"strategy_status_{self.strategy_name}.json")
|
||||
|
||||
|
||||
self.current_signal = "INIT"
|
||||
self.last_signal_change_utc = None
|
||||
self.signal_price = None
|
||||
|
||||
logging.info(f"Initializing with parameters: {self.params}")
|
||||
# Note: Logging is set up by the run_strategy function
|
||||
|
||||
def load_data(self) -> pd.DataFrame:
|
||||
"""Loads historical data for the configured coin and timeframe."""
|
||||
@ -53,27 +60,41 @@ class BaseStrategy(ABC):
|
||||
"""The core logic of the strategy. Must be implemented by child classes."""
|
||||
pass
|
||||
|
||||
def calculate_signals_and_state(self, df: pd.DataFrame):
|
||||
def calculate_signals_and_state(self, df: pd.DataFrame) -> bool:
|
||||
"""
|
||||
A wrapper that calls the strategy's signal calculation and then
|
||||
determines the last signal change from the historical data.
|
||||
A wrapper that calls the strategy's signal calculation, determines
|
||||
the last signal change, and returns True if the signal has changed.
|
||||
"""
|
||||
df_with_signals = self.calculate_signals(df)
|
||||
df_with_signals.dropna(inplace=True)
|
||||
if df_with_signals.empty: return
|
||||
if df_with_signals.empty:
|
||||
return False
|
||||
|
||||
df_with_signals['position_change'] = df_with_signals['signal'].diff()
|
||||
|
||||
last_signal = df_with_signals['signal'].iloc[-1]
|
||||
if last_signal == 1: self.current_signal = "BUY"
|
||||
elif last_signal == -1: self.current_signal = "SELL"
|
||||
else: self.current_signal = "HOLD"
|
||||
last_signal_int = df_with_signals['signal'].iloc[-1]
|
||||
new_signal_str = "HOLD"
|
||||
if last_signal_int == 1: new_signal_str = "BUY"
|
||||
elif last_signal_int == -1: new_signal_str = "SELL"
|
||||
|
||||
last_change_series = df_with_signals[df_with_signals['position_change'] != 0]
|
||||
if not last_change_series.empty:
|
||||
last_change_row = last_change_series.iloc[-1]
|
||||
self.last_signal_change_utc = last_change_row.name.tz_localize('UTC').isoformat()
|
||||
self.signal_price = last_change_row['close']
|
||||
signal_changed = False
|
||||
if self.current_signal == "INIT":
|
||||
if new_signal_str == "BUY": self.current_signal = "INIT_BUY"
|
||||
elif new_signal_str == "SELL": self.current_signal = "INIT_SELL"
|
||||
else: self.current_signal = "HOLD"
|
||||
signal_changed = True
|
||||
elif new_signal_str != self.current_signal:
|
||||
self.current_signal = new_signal_str
|
||||
signal_changed = True
|
||||
|
||||
if signal_changed:
|
||||
last_change_series = df_with_signals[df_with_signals['position_change'] != 0]
|
||||
if not last_change_series.empty:
|
||||
last_change_row = last_change_series.iloc[-1]
|
||||
self.last_signal_change_utc = last_change_row.name.tz_localize('UTC').isoformat()
|
||||
self.signal_price = last_change_row['close']
|
||||
|
||||
return signal_changed
|
||||
|
||||
def _save_status(self):
|
||||
"""Saves the current strategy state to its JSON file."""
|
||||
@ -84,9 +105,62 @@ class BaseStrategy(ABC):
|
||||
"signal_price": self.signal_price,
|
||||
"last_checked_utc": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
# If a shared status dict is provided (Manager.dict()), update it instead of writing files
|
||||
try:
|
||||
with open(self.status_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(status, f, indent=4)
|
||||
if self.shared_status is not None:
|
||||
try:
|
||||
# store the status under the strategy name for easy lookup
|
||||
self.shared_status[self.strategy_name] = status
|
||||
except Exception:
|
||||
# Manager proxies may not accept nested mutable objects consistently; assign a copy
|
||||
self.shared_status[self.strategy_name] = dict(status)
|
||||
else:
|
||||
with open(self.status_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(status, f, indent=4)
|
||||
except IOError as e:
|
||||
logging.error(f"Failed to write status file for {self.strategy_name}: {e}")
|
||||
|
||||
def run_polling_loop(self):
|
||||
"""
|
||||
The default execution loop for polling-based strategies (e.g., SMAs).
|
||||
"""
|
||||
while True:
|
||||
df = self.load_data()
|
||||
if df.empty:
|
||||
logging.warning("No data loaded. Waiting 1 minute...")
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
signal_changed = self.calculate_signals_and_state(df.copy())
|
||||
self._save_status()
|
||||
|
||||
if signal_changed or self.current_signal == "INIT_BUY" or self.current_signal == "INIT_SELL":
|
||||
logging.warning(f"New signal detected: {self.current_signal}")
|
||||
self.trade_signal_queue.put({
|
||||
"strategy_name": self.strategy_name,
|
||||
"signal": self.current_signal,
|
||||
"coin": self.coin,
|
||||
"signal_price": self.signal_price,
|
||||
"config": {"agent": self.params.get("agent"), "parameters": self.params}
|
||||
})
|
||||
if self.current_signal == "INIT_BUY": self.current_signal = "BUY"
|
||||
if self.current_signal == "INIT_SELL": self.current_signal = "SELL"
|
||||
|
||||
logging.info(f"Current Signal: {self.current_signal}")
|
||||
time.sleep(60)
|
||||
|
||||
def run_event_loop(self):
|
||||
"""
|
||||
A placeholder for event-driven (WebSocket) strategies.
|
||||
Child classes must override this.
|
||||
"""
|
||||
logging.error("run_event_loop() is not implemented for this strategy.")
|
||||
time.sleep(3600) # Sleep for an hour to prevent rapid error loops
|
||||
|
||||
def on_fill_message(self, message):
|
||||
"""
|
||||
Placeholder for the WebSocket callback.
|
||||
Child classes must override this.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
178
strategies/copy_trader_strategy.py
Normal file
178
strategies/copy_trader_strategy.py
Normal file
@ -0,0 +1,178 @@
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from hyperliquid.info import Info
|
||||
from hyperliquid.utils import constants
|
||||
|
||||
from strategies.base_strategy import BaseStrategy
|
||||
|
||||
class CopyTraderStrategy(BaseStrategy):
|
||||
"""
|
||||
An event-driven strategy that monitors a target wallet address and
|
||||
copies its trades for a specific set of allowed coins, using
|
||||
per-coin size and leverage settings.
|
||||
"""
|
||||
def __init__(self, strategy_name: str, params: dict, trade_signal_queue, shared_status: dict = None):
|
||||
super().__init__(strategy_name, params, trade_signal_queue, shared_status)
|
||||
|
||||
self.target_address = self.params.get("target_address", "").lower()
|
||||
|
||||
self.coins_to_copy = self.params.get("coins_to_copy", {})
|
||||
self.allowed_coins = list(self.coins_to_copy.keys())
|
||||
|
||||
if not self.target_address:
|
||||
logging.error("No 'target_address' specified in parameters for copy trader.")
|
||||
raise ValueError("target_address is required")
|
||||
if not self.allowed_coins:
|
||||
logging.warning("No 'coins_to_copy' configured. This strategy will not copy any trades.")
|
||||
|
||||
self.info = None # Will be initialized in the run loop
|
||||
|
||||
# --- FIX: Set initial state to "WAIT" ---
|
||||
self.current_signal = "WAIT"
|
||||
|
||||
# Record the strategy's start time to ignore historical data
|
||||
self.start_time_utc = datetime.now(timezone.utc)
|
||||
logging.info(f"Strategy initialized. Ignoring all trades before {self.start_time_utc.isoformat()}")
|
||||
|
||||
def calculate_signals(self, df):
|
||||
# This strategy is event-driven, so it does not use polling-based signal calculation.
|
||||
pass
|
||||
|
||||
def on_fill_message(self, message):
|
||||
"""
|
||||
This is the callback function that gets triggered by the WebSocket
|
||||
every time the monitored address has an event.
|
||||
"""
|
||||
try:
|
||||
channel = message.get("channel")
|
||||
if channel not in ("user", "userFills", "userEvents"):
|
||||
return
|
||||
|
||||
data = message.get("data")
|
||||
if not data:
|
||||
return
|
||||
|
||||
fills = data.get("fills", [])
|
||||
if not fills:
|
||||
return
|
||||
|
||||
user_address = data.get("user", "").lower()
|
||||
|
||||
if user_address != self.target_address:
|
||||
return
|
||||
|
||||
logging.debug(f"Received {len(fills)} fill(s) for user {user_address}")
|
||||
|
||||
for fill in fills:
|
||||
# Check if the trade is new or historical
|
||||
trade_time = datetime.fromtimestamp(fill['time'] / 1000, tz=timezone.utc)
|
||||
if trade_time < self.start_time_utc:
|
||||
logging.info(f"Ignoring stale/historical trade from {trade_time.isoformat()}")
|
||||
continue
|
||||
|
||||
coin = fill.get('coin')
|
||||
|
||||
if coin in self.allowed_coins:
|
||||
side = fill.get('side')
|
||||
price = float(fill.get('px'))
|
||||
|
||||
signal = "HOLD"
|
||||
if side == "B":
|
||||
signal = "BUY"
|
||||
elif side == "A":
|
||||
signal = "SELL"
|
||||
|
||||
coin_config = self.coins_to_copy.get(coin)
|
||||
if not coin_config or not coin_config.get("size"):
|
||||
logging.warning(f"No trade size specified for {coin}. Ignoring fill.")
|
||||
continue
|
||||
|
||||
# --- 1. Create the trade-specific config ---
|
||||
trade_params = self.params.copy()
|
||||
trade_params.update(coin_config)
|
||||
trade_config = {
|
||||
"agent": self.params.get("agent"),
|
||||
"parameters": trade_params
|
||||
}
|
||||
|
||||
# --- 2. (PRIORITY) Put the signal on the queue for the executor ---
|
||||
self.trade_signal_queue.put({
|
||||
"strategy_name": self.strategy_name,
|
||||
"signal": signal,
|
||||
"coin": coin,
|
||||
"signal_price": price,
|
||||
"config": trade_config
|
||||
})
|
||||
|
||||
# --- 3. (Secondary) Update internal state and log ---
|
||||
self.current_signal = signal
|
||||
self.signal_price = price
|
||||
self.last_signal_change_utc = trade_time.isoformat()
|
||||
self._save_status() # Update the dashboard status file
|
||||
|
||||
logging.warning(f"Copy trade signal SENT for {coin}: {signal} @ {price}, Size: {coin_config['size']}")
|
||||
logging.info(f"Source trade logged: {json.dumps(fill)}")
|
||||
|
||||
else:
|
||||
logging.info(f"Ignoring fill for unmonitored coin: {coin}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in on_fill_message: {e}", exc_info=True)
|
||||
|
||||
def _connect_and_subscribe(self):
|
||||
"""
|
||||
Establishes a new WebSocket connection and subscribes to the userFills channel.
|
||||
"""
|
||||
try:
|
||||
logging.info("Connecting to Hyperliquid WebSocket...")
|
||||
self.info = Info(constants.MAINNET_API_URL, skip_ws=False)
|
||||
subscription = {"type": "userFills", "user": self.target_address}
|
||||
self.info.subscribe(subscription, self.on_fill_message)
|
||||
logging.info(f"Subscribed to 'userFills' for target address: {self.target_address}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to connect or subscribe: {e}")
|
||||
self.info = None
|
||||
return False
|
||||
|
||||
def run_event_loop(self):
|
||||
"""
|
||||
This method overrides the default polling loop. It establishes a
|
||||
persistent WebSocket connection and runs a watchdog to ensure
|
||||
it stays connected.
|
||||
"""
|
||||
if not self._connect_and_subscribe():
|
||||
# If connection fails on start, wait 60s before letting the process restart
|
||||
time.sleep(60)
|
||||
return
|
||||
|
||||
# --- ADDED: Save the initial "WAIT" status ---
|
||||
self._save_status()
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(15) # Check the connection every 15 seconds
|
||||
|
||||
if self.info is None or not self.info.ws_manager.is_alive():
|
||||
logging.error(f"WebSocket connection lost. Attempting to reconnect...")
|
||||
|
||||
if self.info and self.info.ws_manager:
|
||||
try:
|
||||
self.info.ws_manager.stop()
|
||||
except Exception as e:
|
||||
logging.error(f"Error stopping old ws_manager: {e}")
|
||||
|
||||
if not self._connect_and_subscribe():
|
||||
logging.error("Reconnect failed, will retry in 15s.")
|
||||
else:
|
||||
logging.info("Successfully reconnected to WebSocket.")
|
||||
# After reconnecting, save the current status again
|
||||
self._save_status()
|
||||
else:
|
||||
logging.debug("Watchdog check: WebSocket connection is active.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred in the watchdog loop: {e}", exc_info=True)
|
||||
|
||||
@ -7,8 +7,10 @@ class MaCrossStrategy(BaseStrategy):
|
||||
A strategy based on a fast Simple Moving Average (SMA) crossing
|
||||
a slow SMA.
|
||||
"""
|
||||
def __init__(self, strategy_name: str, params: dict, log_level: str):
|
||||
super().__init__(strategy_name, params)
|
||||
# --- FIX: Changed 3rd argument from log_level to trade_signal_queue ---
|
||||
def __init__(self, strategy_name: str, params: dict, trade_signal_queue):
|
||||
# --- FIX: Passed trade_signal_queue to the parent class ---
|
||||
super().__init__(strategy_name, params, trade_signal_queue)
|
||||
self.fast_ma_period = self.params.get('short_ma') or self.params.get('fast') or 0
|
||||
self.slow_ma_period = self.params.get('long_ma') or self.params.get('slow') or 0
|
||||
|
||||
@ -26,4 +28,3 @@ class MaCrossStrategy(BaseStrategy):
|
||||
df.loc[df['fast_sma'] < df['slow_sma'], 'signal'] = -1
|
||||
|
||||
return df
|
||||
|
||||
|
||||
@ -6,8 +6,10 @@ class SingleSmaStrategy(BaseStrategy):
|
||||
"""
|
||||
A strategy based on the price crossing a single Simple Moving Average (SMA).
|
||||
"""
|
||||
def __init__(self, strategy_name: str, params: dict):
|
||||
super().__init__(strategy_name, params)
|
||||
# --- FIX: Added trade_signal_queue to the constructor ---
|
||||
def __init__(self, strategy_name: str, params: dict, trade_signal_queue):
|
||||
# --- FIX: Passed trade_signal_queue to the parent class ---
|
||||
super().__init__(strategy_name, params, trade_signal_queue)
|
||||
self.sma_period = self.params.get('sma_period', 0)
|
||||
|
||||
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
@ -23,4 +25,3 @@ class SingleSmaStrategy(BaseStrategy):
|
||||
df.loc[df['close'] < df['sma'], 'signal'] = -1
|
||||
|
||||
return df
|
||||
|
||||
|
||||
Reference in New Issue
Block a user