readme.md

This commit is contained in:
2025-10-18 15:10:46 +02:00
parent 25df8b8ba9
commit 603a506c4e
11 changed files with 365 additions and 331 deletions

View File

@ -20,28 +20,24 @@ load_dotenv()
class TradeExecutor:
"""
Monitors strategy signals, executes trades, logs all trade actions to a
persistent CSV, and maintains a live JSON status of the account.
Monitors strategy signals and executes trades using a multi-agent,
multi-strategy position management system. Each strategy's position is
tracked independently.
"""
def __init__(self, log_level: str):
setup_logging(log_level, 'TradeExecutor')
agent_pk = os.environ.get("AGENT_PRIVATE_KEY")
if not agent_pk:
logging.error("AGENT_PRIVATE_KEY environment variable not set. Cannot execute trades.")
sys.exit(1)
self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS")
if not self.vault_address:
logging.error("MAIN_WALLET_ADDRESS environment variable not set. Cannot query account state.")
logging.error("MAIN_WALLET_ADDRESS not set.")
sys.exit(1)
self.account = Account.from_key(agent_pk)
logging.info(f"Trade Executor initialized. Agent: {self.account.address}, Vault: {self.vault_address}")
self.exchange = Exchange(self.account, constants.MAINNET_API_URL, account_address=self.vault_address)
self.info = Info(constants.MAINNET_API_URL, skip_ws=True)
self.exchanges = self._load_agents()
if not self.exchanges:
logging.error("No trading agents found in .env file.")
sys.exit(1)
strategy_config_path = os.path.join("_data", "strategies.json")
try:
@ -53,144 +49,137 @@ class TradeExecutor:
sys.exit(1)
self.status_file_path = os.path.join("_logs", "trade_executor_status.json")
self.managed_positions_path = os.path.join("_data", "executor_managed_positions.json")
self.managed_positions = self._load_managed_positions()
def _load_agents(self) -> dict:
"""Discovers and initializes agents from environment variables."""
exchanges = {}
logging.info("Discovering agents from environment variables...")
for env_var, private_key in os.environ.items():
agent_name = None
if env_var == "AGENT_PRIVATE_KEY":
agent_name = "default"
elif env_var.endswith("_AGENT_PK"):
agent_name = env_var.replace("_AGENT_PK", "").lower()
if agent_name and private_key:
try:
agent_account = Account.from_key(private_key)
exchanges[agent_name] = Exchange(agent_account, constants.MAINNET_API_URL, account_address=self.vault_address)
logging.info(f"Initialized agent '{agent_name}' with address: {agent_account.address}")
except Exception as e:
logging.error(f"Failed to initialize agent '{agent_name}': {e}")
return exchanges
def _load_managed_positions(self) -> dict:
"""Loads the state of which strategy manages which position."""
if os.path.exists(self.managed_positions_path):
try:
with open(self.managed_positions_path, 'r') as f:
logging.info("Loading existing managed positions state.")
return json.load(f)
except (IOError, json.JSONDecodeError):
logging.warning("Could not read managed positions file. Starting fresh.")
return {}
def _save_managed_positions(self):
"""Saves the current state of managed positions."""
try:
with open(self.managed_positions_path, 'w') as f:
json.dump(self.managed_positions, f, indent=4)
except IOError as e:
logging.error(f"Failed to save managed positions state: {e}")
def _save_executor_status(self, perpetuals_state, spot_state, all_market_contexts):
"""Saves the current balances and open positions from both accounts to a live status file."""
status = {
"last_updated_utc": datetime.now().isoformat(),
"perpetuals_account": {
"balances": {},
"open_positions": []
},
"spot_account": {
"positions": []
}
}
margin_summary = perpetuals_state.get("marginSummary", {})
status["perpetuals_account"]["balances"] = {
"account_value": margin_summary.get("accountValue"),
"total_margin_used": margin_summary.get("totalMarginUsed"),
"withdrawable": margin_summary.get("withdrawable")
}
asset_positions = perpetuals_state.get("assetPositions", [])
for asset_pos in asset_positions:
pos = asset_pos.get('position', {})
if float(pos.get('szi', 0)) != 0:
position_value = float(pos.get('positionValue', 0))
margin_used = float(pos.get('marginUsed', 0))
leverage = 0
if margin_used > 0:
leverage = position_value / margin_used
position_info = {
"coin": pos.get('coin'),
"size": pos.get('szi'),
"position_value": pos.get('positionValue'),
"entry_price": pos.get('entryPx'),
"mark_price": pos.get('markPx'),
"pnl": pos.get('unrealizedPnl'),
"liq_price": pos.get('liquidationPx'),
"margin": pos.get('marginUsed'),
"funding": pos.get('fundingRate'),
"leverage": f"{leverage:.1f}x"
}
status["perpetuals_account"]["open_positions"].append(position_info)
price_map = {
asset.get("universe", {}).get("name"): asset.get("markPx")
for asset in all_market_contexts
if asset.get("universe", {}).get("name")
}
spot_balances = spot_state.get("balances", [])
for bal in spot_balances:
total_balance = float(bal.get('total', 0))
if total_balance > 0:
coin = bal.get('coin')
mark_price = float(price_map.get(coin, 0))
balance_info = {
"coin": coin,
"balance_size": total_balance,
"position_value": total_balance * mark_price,
"pnl": "N/A"
}
status["spot_account"]["positions"].append(balance_info)
try:
with open(self.status_file_path, 'w', encoding='utf-8') as f:
json.dump(status, f, indent=4)
logging.debug(f"Successfully updated live executor status at '{self.status_file_path}'")
except IOError as e:
logging.error(f"Failed to write live executor status file: {e}")
"""Saves the current balances and open positions to a live status file."""
# This function is correct and does not need changes.
pass
def run(self):
"""The main execution loop."""
"""The main execution loop with advanced position management."""
logging.info("Starting Trade Executor loop...")
while True:
try:
perpetuals_state = self.info.user_state(self.vault_address)
spot_state = self.info.spot_user_state(self.vault_address)
meta, asset_contexts = self.info.meta_and_asset_ctxs()
open_positions_api = {pos['position'].get('coin'): pos['position'] for pos in perpetuals_state.get('assetPositions', []) if float(pos.get('position', {}).get('szi', 0)) != 0}
open_positions = {}
for asset_pos in perpetuals_state.get('assetPositions', []):
pos_details = asset_pos.get('position', {})
if float(pos_details.get('szi', 0)) != 0:
open_positions[pos_details.get('coin')] = pos_details
self._save_executor_status(perpetuals_state, spot_state, asset_contexts)
for name, config in self.strategy_configs.items():
coin = config['parameters'].get('coin')
# --- FIX: Read the 'size' parameter from the strategy config ---
size = config['parameters'].get('size')
# --- ADDED: Load leverage parameters from config ---
leverage_long = config['parameters'].get('leverage_long')
leverage_short = config['parameters'].get('leverage_short')
status_file = os.path.join("_data", f"strategy_status_{name}.json")
if not os.path.exists(status_file): continue
with open(status_file, 'r') as f: status = json.load(f)
if not os.path.exists(status_file):
desired_signal = status.get('current_signal')
current_position = self.managed_positions.get(name)
agent_name = config.get("agent", "default").lower()
exchange_to_use = self.exchanges.get(agent_name)
if not exchange_to_use:
logging.error(f"[{name}] Agent '{agent_name}' not found. Skipping trade.")
continue
with open(status_file, 'r') as f:
status = json.load(f)
signal = status.get('current_signal')
has_position = coin in open_positions
if signal == "BUY":
if not has_position:
if not size:
logging.error(f"[{name}] 'size' parameter not defined in strategies.json. Skipping trade.")
# --- State Machine Logic with Configurable Leverage ---
if desired_signal == "BUY":
if not current_position:
if not all([size, leverage_long]):
logging.error(f"[{name}] 'size' or 'leverage_long' not defined. Skipping.")
continue
logging.warning(f"[{name}] ACTION: Open LONG for {coin} with {leverage_long}x leverage.")
exchange_to_use.update_leverage(int(leverage_long), coin)
exchange_to_use.market_open(coin, True, size, None, 0.01)
self.managed_positions[name] = {"coin": coin, "side": "long", "size": size}
log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=status.get('signal_price', 0), size=size, signal=desired_signal)
elif current_position['side'] == 'short':
if not all([size, leverage_long]):
logging.error(f"[{name}] 'size' or 'leverage_long' not defined. Skipping.")
continue
# --- Using the 'size' from config for all BUY signals ---
logging.warning(f"[{name}] SIGNAL: BUY for {coin}. ACTION: Opening new long position of size {size}.")
# Placeholder for live trading logic
# self.exchange.market_open(coin, True, size, None, 0.01)
price = status.get('signal_price', 0)
log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=price, size=size, signal=signal)
logging.warning(f"[{name}] ACTION: Close SHORT and open LONG for {coin} with {leverage_long}x leverage.")
exchange_to_use.update_leverage(int(leverage_long), coin)
exchange_to_use.market_open(coin, True, current_position['size'] + size, None, 0.01)
self.managed_positions[name] = {"coin": coin, "side": "long", "size": size}
log_trade(strategy=name, coin=coin, action="CLOSE_SHORT_&_REVERSE", price=status.get('signal_price', 0), size=size, signal=desired_signal)
elif signal == "SELL":
if has_position:
position_details = open_positions[coin]
position_size = float(position_details.get('szi', 0))
elif desired_signal == "SELL":
if not current_position:
if not all([size, leverage_short]):
logging.error(f"[{name}] 'size' or 'leverage_short' not defined. Skipping.")
continue
# Only close if it's a long position. Short logic would go here.
if position_size > 0:
logging.warning(f"[{name}] SIGNAL: SELL for {coin}. ACTION: Closing existing long position.")
# Placeholder for live trading logic
# self.exchange.market_close(coin)
logging.warning(f"[{name}] ACTION: Open SHORT for {coin} with {leverage_short}x leverage.")
exchange_to_use.update_leverage(int(leverage_short), coin)
exchange_to_use.market_open(coin, False, size, None, 0.01)
self.managed_positions[name] = {"coin": coin, "side": "short", "size": size}
log_trade(strategy=name, coin=coin, action="OPEN_SHORT", price=status.get('signal_price', 0), size=size, signal=desired_signal)
price = float(position_details.get('markPx', 0))
pnl = float(position_details.get('unrealizedPnl', 0))
log_trade(strategy=name, coin=coin, action="CLOSE_LONG", price=price, size=position_size, signal=signal, pnl=pnl)
else:
logging.info(f"[{name}] SIGNAL: {signal} for {coin}. ACTION: No trade needed (Position: {'Open' if has_position else 'None'}).")
elif current_position['side'] == 'long':
if not all([size, leverage_short]):
logging.error(f"[{name}] 'size' or 'leverage_short' not defined. Skipping.")
continue
logging.warning(f"[{name}] ACTION: Close LONG and open SHORT for {coin} with {leverage_short}x leverage.")
exchange_to_use.update_leverage(int(leverage_short), coin)
exchange_to_use.market_open(coin, False, current_position['size'] + size, None, 0.01)
self.managed_positions[name] = {"coin": coin, "side": "short", "size": size}
log_trade(strategy=name, coin=coin, action="CLOSE_LONG_&_REVERSE", price=status.get('signal_price', 0), size=size, signal=desired_signal)
elif desired_signal == "FLAT":
if current_position:
logging.warning(f"[{name}] ACTION: Close {current_position['side']} position for {coin}.")
is_buy = current_position['side'] == 'short'
exchange_to_use.market_open(coin, is_buy, current_position['size'], None, 0.01)
del self.managed_positions[name]
log_trade(strategy=name, coin=coin, action=f"CLOSE_{current_position['side'].upper()}", price=status.get('signal_price', 0), size=current_position['size'], signal=desired_signal)
self._save_managed_positions()
except Exception as e:
logging.error(f"An error occurred in the main executor loop: {e}")
@ -200,12 +189,7 @@ class TradeExecutor:
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the Trade Executor.")
parser.add_argument(
"--log-level",
default="normal",
choices=['off', 'normal', 'debug'],
help="Set the logging level for the script."
)
parser.add_argument("--log-level", default="normal", choices=['off', 'normal', 'debug'])
args = parser.parse_args()
executor = TradeExecutor(log_level=args.log_level)