Full Example Script

The main() function ties everything together:

  1. Login and get an access token

  2. Monitor prices continuously (in this example we're trading XRP due to its low minimum acceptable quote value)

  3. Open a position when the price drops below the entry threshold

  4. Monitor the position status to get a permanent quote ID

  5. Close the position when the price rises above the exit threshold

  6. Handle errors and retry as needed

This trading bot implements a simple "buy low, sell high" strategy, but entry/exit conditions can be modified for more advanced strategies.

To help guide users in setting up the necessary environment variables, the repository includes a .env.example file. This file provides a template outlining the format and naming of the required variables. Users should copy this file, rename it to .env, and fill in the appropriate values specific to their setup. This ensures that all required configurations are consistently specified for the trading bot to function correctly.

Full Script

import os
import time
import requests
import json
from eth_account import Account
from eth_account.messages import encode_defunct
from dotenv import load_dotenv
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import traceback

# Configuration
CONFIG = {
    "SYMBOL": "XRPUSDT",   # Trading pair on Binance
    "ENTRY_PRICE": "3.05",  # Price to enter the position (adjust as needed)
    "EXIT_PRICE": "3.1",   # Price to exit the position (adjust as needed)
    "QUANTITY": "6",      # Amount of XRP to trade
    "POSITION_TYPE": 0,     # 0 for long, 1 for short
    "SYMBOL_ID": 340,       # XRP symbol ID in Symmio
    "LEVERAGE": "1",        # Leverage value
    "MAX_FUNDING_RATE": "200",
    "DEADLINE_OFFSET": 3600,  # 1 hour
    "POLL_INTERVAL": 5,       # Seconds between price checks
    "STATUS_POLL_INTERVAL": 0.5  # Seconds between status checks
}

# Load environment variables
load_dotenv()

# Environment variables
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
ACTIVE_ACCOUNT = os.getenv("SUB_ACCOUNT_ADDRESS")
HEDGER_URL = os.getenv("HEDGER_URL")
CHAIN_ID = int(os.getenv("CHAIN_ID", 42161))
MUON_BASE_URL = os.getenv("MUON_BASE_URL")
DOMAIN = "localhost"
ORIGIN = "http://localhost:3000"
LOGIN_URI = f"{HEDGER_URL}/login"
DIAMOND_ADDRESS = os.getenv("DIAMOND_ADDRESS")

# URLs
BINANCE_API_URL = "https://api.binance.com/api/v3/ticker/price"
LOCKED_PARAMS_URL = f"{HEDGER_URL}/get_locked_params/XRPUSDT?leverage={CONFIG['LEVERAGE']}"
MUON_URL = f"{MUON_BASE_URL}?app=symmio&method=uPnl_A_withSymbolPrice&params[partyA]={ACTIVE_ACCOUNT}&params[chainId]={CHAIN_ID}&params[symmio]={DIAMOND_ADDRESS}&params[symbolId]={CONFIG['SYMBOL_ID']}"
STATUS_URL = f"{HEDGER_URL}/instant_open/{ACTIVE_ACCOUNT}"

# Initialize wallet
wallet = Account.from_key(PRIVATE_KEY)

# Timestamp formats
ISSUED_AT = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
EXPIRATION_DATE = (datetime.now(timezone.utc) + timedelta(hours=2, minutes=30)).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

def get_nonce(address: str) -> str:
    """Fetch the nonce for the active account from the server."""
    url = f"{HEDGER_URL}/nonce/{address}"
    response = requests.get(url)
    response.raise_for_status()
    return response.json()["nonce"]

def build_siwe_message(domain, address, statement, uri, version, chain_id, nonce, issued_at, expiration_time):
    """Build a SIWE message string following the EIP-4361 format."""
    return f"""{domain} wants you to sign in with your Ethereum account:
{address}

{statement}

URI: {uri}
Version: {version}
Chain ID: {chain_id}
Nonce: {nonce}
Issued At: {issued_at}
Expiration Time: {expiration_time}"""

def login():
    """Perform SIWE login and return the access token."""
    try:
        print(f"[LOGIN] Wallet Address: {wallet.address}")
        print(f"[LOGIN] Active Account: {ACTIVE_ACCOUNT}")

        # Fetch the nonce for the active account from the server
        nonce = get_nonce(ACTIVE_ACCOUNT)
        print(f"[LOGIN] Got nonce: {nonce}")

        # Create the SIWE message manually
        message_string = build_siwe_message(
            domain=DOMAIN,
            address=wallet.address,
            statement=f"msg: {ACTIVE_ACCOUNT}",
            uri=LOGIN_URI,
            version="1",
            chain_id=CHAIN_ID,
            nonce=nonce,
            issued_at=ISSUED_AT,
            expiration_time=EXPIRATION_DATE
        )

        message = encode_defunct(text=message_string)
        
        signed_message = wallet.sign_message(message)
        signature = "0x" + signed_message.signature.hex()

        body = {
            "account_address": ACTIVE_ACCOUNT,
            "expiration_time": EXPIRATION_DATE,
            "issued_at": ISSUED_AT,
            "signature": signature,
            "nonce": nonce
        }

        headers = {
            "Content-Type": "application/json",
            "Origin": ORIGIN,
            "Referer": ORIGIN,
        }

        print("[LOGIN] Sending login request...")

        response = requests.post(
            LOGIN_URI,
            json=body,
            headers=headers
        )
        
        response.raise_for_status()
        token = response.json().get("access_token")
        print(f"[LOGIN] Successfully obtained access token")
        return token
    except Exception as e:
        print(f"[ERROR] Login failed: {e}")
        traceback.print_exc()
        return None

def get_binance_price(symbol):
    """Get the current price of a symbol from Binance."""
    try:
        params = {"symbol": symbol}
        response = requests.get(BINANCE_API_URL, params=params)
        response.raise_for_status()
        data = response.json()
        price = Decimal(data["price"])
        print(f"[PRICE] Current {symbol} price: {price}")
        return price
    except Exception as e:
        print(f"[ERROR] Failed to get Binance price: {e}")
        return None

def fetch_muon_price():
    """Fetch the current price from Muon oracle."""
    try:
        response = requests.get(MUON_URL)
        response.raise_for_status()
        data = response.json()
        fetched_price_wei = data["result"]["data"]["result"]["price"]
        if not fetched_price_wei:
            raise ValueError("Muon price not found in response.")
        return Decimal(fetched_price_wei) / Decimal('1e18')
    except Exception as e:
        print(f"[ERROR] Failed to fetch Muon price: {e}")
        return None

def fetch_locked_params():
    """Fetch locked parameters for the trade."""
    try:
        response = requests.get(LOCKED_PARAMS_URL)
        response.raise_for_status()
        data = response.json()
        if data.get("message") == "Success":
            return data
        else:
            raise ValueError("Failed to fetch locked parameters")
    except Exception as e:
        print(f"[ERROR] Failed to fetch locked parameters: {e}")
        return None

def calculate_normalized_locked_value(notional, locked_param, leverage, apply_leverage=True):
    """Compute normalized locked value for a given parameter."""
    notional = Decimal(str(notional))
    locked_param = Decimal(str(locked_param))
    leverage = Decimal(str(leverage))
    
    if apply_leverage:
        return str(notional * locked_param / (Decimal('100') * leverage))
    else:
        return str(notional * locked_param / Decimal('100'))

def open_instant_trade(token):
    """Execute an instant open trade using the access token. Returns temp_quote_id."""
    try:
        print("[TRADE] Starting instant open process...")
        
        fetched_price = fetch_muon_price()
        if not fetched_price:
            return None
        print(f"[TRADE] Fetched price: {fetched_price}")
        
        # Because in this example we are going LONG, we are increasing the price by 1% before we send the order
        adjusted_price = fetched_price * Decimal('1.01')
        print(f"[TRADE] Adjusted price (+1%): {adjusted_price}")
        
        locked_params = fetch_locked_params()
        if not locked_params:
            return None
        
        notional = adjusted_price * Decimal(CONFIG["QUANTITY"])
        print(f"[TRADE] Notional: {notional}")
        
        leverage = Decimal(locked_params["leverage"])
        normalized_cva = calculate_normalized_locked_value(notional, locked_params["cva"], leverage, True)
        normalized_lf = calculate_normalized_locked_value(notional, locked_params["lf"], leverage, True)
        normalized_party_amm = calculate_normalized_locked_value(notional, locked_params["partyAmm"], leverage, True)
        
        deadline = int(time.time()) + CONFIG["DEADLINE_OFFSET"]
        trade_params = {
            "symbolId": CONFIG["SYMBOL_ID"],
            "positionType": CONFIG["POSITION_TYPE"],
            "orderType": 1,  # 1 for market order
            "price": str(adjusted_price),
            "quantity": CONFIG["QUANTITY"],
            "cva": normalized_cva,
            "lf": normalized_lf,
            "partyAmm": normalized_party_amm,
            "partyBmm": '0',
            "maxFundingRate": CONFIG["MAX_FUNDING_RATE"],
            "deadline": deadline
        }
        
        print(f"[TRADE] Trade Payload: {trade_params}")
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        }
        
        response = requests.post(f"{HEDGER_URL}/instant_open", json=trade_params, headers=headers)
        
        print(f"[TRADE] Response status: {response.status_code}")
        print(f"[TRADE] Response: {response.text}")
        
        response.raise_for_status()
        result = response.json()
        
        temp_quote_id = result.get("quote_id")
        if temp_quote_id:
            print(f"[TRADE] Received temporary quote ID: {temp_quote_id}")
            return temp_quote_id
        else:
            print("[ERROR] No temporary quote ID in response")
            return None
            
    except Exception as e:
        print(f"[ERROR] Failed to open instant trade: {e}")
        traceback.print_exc()
        return None

def poll_quote_status(token, temp_quote_id):
    """Poll for the status of a quote until it gets a permanent ID.""" #In order to track the status of the quote, we will poll the /instant_open/{address} endpoint
    print(f"[STATUS] Starting to poll for quote status of temp ID: {temp_quote_id}")
    
    headers = {
        "Authorization": f"Bearer {token}"
    }
    
    max_attempts = 120  # 60 seconds (120 * 0.5s)
    attempts = 0
    
    if isinstance(temp_quote_id, str) and temp_quote_id.startswith('-'):
        try:
            temp_quote_id = int(temp_quote_id)
        except ValueError:
            print(f"[STATUS] Warning: Could not convert temp_quote_id {temp_quote_id} to int")
    
    print(f"[STATUS] Looking for temp_quote_id: {temp_quote_id} (type: {type(temp_quote_id)})")
    
    while attempts < max_attempts:
        try:
            response = requests.get(STATUS_URL, headers=headers)
            if response.status_code != 200:
                print(f"[STATUS] Error: Response status {response.status_code}")
                print(f"[STATUS] Response text: {response.text}")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            data = response.json()
            print(f"[STATUS] Poll attempt {attempts+1}/{max_attempts}")
            
            if not data:
                print("[STATUS] Empty response, waiting for next update...")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                            
            quotes = []
            if isinstance(data, list):
                quotes = data
            elif isinstance(data, dict) and "quotes" in data:
                quotes = data["quotes"]
            
            if not quotes:
                print("[STATUS] No quotes found in response")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            print(f"[STATUS] Found {len(quotes)} quotes to check")
            
            # Look for any quote with a positive quote_id (confirmed)
            for quote in quotes:
                quote_id = quote.get("quote_id")
            
                if isinstance(quote_id, str) and quote_id.isdigit():
                    quote_id = int(quote_id)
                
                if isinstance(quote_id, int) and quote_id > 0:
                    print(f"[STATUS] ✓ CONFIRMED: Quote has permanent ID: {quote_id}")
                    return quote_id
            
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
            
        except Exception as e:
            print(f"[ERROR] Error polling quote status: {e}")
            traceback.print_exc()
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
    
    print("[STATUS] âš  Timed out waiting for permanent quote ID")
    return None

def close_instant_position(token, quote_id, current_price):
    """Close an open position."""
    try:
        print(f"[CLOSE] Preparing to close position with quote ID: {quote_id}")
        
        # Fetch Muon price and apply -1% slippage (for long positions)
        muon_price = fetch_muon_price()
        if not muon_price:
            return False
            
        close_price = str(muon_price * Decimal("0.99"))  # 1% slippage for selling
        print(f"[CLOSE] Close Price: {close_price}")
        
        payload = {
            "quote_id": quote_id,
            "quantity_to_close": CONFIG["QUANTITY"],
            "close_price": close_price
        }
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        }
        
        print("[CLOSE] Sending instant close request...")
        response = requests.post(f"{HEDGER_URL}/instant_close", json=payload, headers=headers)
        
        print(f"[CLOSE] Response status: {response.status_code}")
        print(f"[CLOSE] Response: {response.text}")
        
        response.raise_for_status()
        print("[CLOSE] Position closed successfully")
        return True
        
    except Exception as e:
        print(f"[ERROR] Failed to close position: {e}")
        traceback.print_exc()
        return False

def main():
    """Main trading bot logic."""
    try:
        print("=============================================")
        print("XRP Trading Bot Starting")
        print("=============================================")
        print(f"Entry Price: {CONFIG['ENTRY_PRICE']}")
        print(f"Exit Price: {CONFIG['EXIT_PRICE']}")
        print(f"Quantity: {CONFIG['QUANTITY']}")
        print("=============================================")
        
        # Login and get access token
        access_token = login()
        if not access_token:
            print("[ERROR] Failed to login. Exiting.")
            return
        
        # Entry price as Decimal for comparison
        entry_price = Decimal(CONFIG["ENTRY_PRICE"])
        exit_price = Decimal(CONFIG["EXIT_PRICE"])
        
        # Trading state
        in_position = False
        confirmed_quote_id = None
        temp_quote_id = None
        
        print("[BOT] Starting price monitoring loop...")
        
        # Main trading loop
        while True:
            try:
                # Get current price from Binance
                current_price = get_binance_price(CONFIG["SYMBOL"])
                
                if not current_price:
                    print("[WARNING] Failed to get price, retrying...")
                    time.sleep(CONFIG["POLL_INTERVAL"])
                    continue
                
                # If not in a position and price is below entry price, enter position
                if not in_position and current_price <= entry_price:
                    print(f"[SIGNAL] Entry signal triggered at price {current_price}")
                    
                    # Execute the trade
                    temp_quote_id = open_instant_trade(access_token)
                    
                    if temp_quote_id:
                        print(f"[BOT] Trade executed with temporary quote ID: {temp_quote_id}")
                        
                        # Poll for the permanent quote ID
                        confirmed_quote_id = poll_quote_status(access_token, temp_quote_id)
                        
                        if confirmed_quote_id:
                            print(f"[BOT] Quote confirmed with ID: {confirmed_quote_id}")
                            in_position = True
                        else:
                            print("[WARNING] Failed to get confirmed quote ID. Will retry on next cycle.")
                    else:
                        print("[ERROR] Failed to execute trade")
                
                elif in_position and current_price >= exit_price:
                    print(f"[SIGNAL] Exit signal triggered at price {current_price}")
                    
                    success = close_instant_position(access_token, confirmed_quote_id, current_price)
                    
                    if success:
                        print("[BOT] Position closed successfully")
                        in_position = False
                        confirmed_quote_id = None
                        temp_quote_id = None
                        print("[BOT] Waiting for next entry opportunity...")
                    else:
                        print("[ERROR] Failed to close position. Will retry.")
                
                time.sleep(CONFIG["POLL_INTERVAL"])
                
            except Exception as e:
                print(f"[ERROR] Error in main trading loop: {e}")
                traceback.print_exc()
                time.sleep(CONFIG["POLL_INTERVAL"])
        
    except KeyboardInterrupt:
        print("\n[BOT] Trading bot stopped by user")
    except Exception as e:
        print(f"[ERROR] Unhandled exception: {e}")
        traceback.print_exc()

if __name__ == "__main__":
    main()

Last updated