```
import asyncio
import logging
import os
import time as pytime
from decimal import Decimal
from dotenv import load_dotenv
from lighter import SignerClient, TransactionApi, ApiClient
import requests
import socket
import traceback
import json
from pydantic import StrictInt
from lighter.models.order_book_details import OrderBookDetails
import time
# --- Logging ---
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# --- Load env ---
load_dotenv()
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
MARKET = os.getenv("MARKET", "ETH")
BASE_URL = "https://mainnet.zklighter.elliot.ai"
SPREAD = Decimal("0.000625") # Increased to $2.50 at $4000 ETH (from 0.00025 = $1)
REFRESH_INTERVAL = 10
MARKET_ID = 0
ACCOUNT_INDEX = 89181
API_KEY_INDEX = 3
STOP_LOSS_PNL = Decimal("-0.025") # Stop-loss at -$0.025 (1.5x avg win of $0.0171)
ATR_THRESHOLD = Decimal("0.9") # Volatility threshold for ATR
ATR_PERIOD = 14 # Standard 14-period ATR for 1-minute data
# --- Manual Nonce Manager (fallback) ---
class ManualNonceManager:
def __init__(self, base_url, api_key_index=API_KEY_INDEX, account_index=ACCOUNT_INDEX):
self.base_url = base_url.rstrip('/')
self.api_key_index = api_key_index
self.account_index = account_index
self.current_nonce = 0
self.last_fetch_time = 0
self.fetch_interval = 60
def get_next_nonce(self):
current_time = pytime.time()
if (current_time - self.last_fetch_time) > self.fetch_interval:
try:
nonce = self._fetch_nonce_from_api()
if nonce is not None:
self.current_nonce = nonce
self.last_fetch_time = current_time
logger.info(f"🔄 Fetched fresh nonce: {nonce}")
except Exception as e:
logger.warning(f"Failed to fetch fresh nonce: {e}")
self.current_nonce += 1
return self.current_nonce
def _fetch_nonce_from_api(self):
url = f"{self.base_url}/api/v1/nextNonce"
params = {'account_index': self.account_index, 'api_key_index': self.api_key_index}
try:
response = requests.get(url, params=params, timeout=10)
logger.debug(f"Nonce response: {response.status_code} - {response.text}")
if response.status_code == 200:
data = response.json()
return int(data['nonce']) if 'nonce' in data else None
return None
except Exception:
return None
# --- Connectivity Check ---
def validate_connectivity(base_url):
logger.info("=== Validating connectivity ===")
host = base_url.split('://')[1].split('/')[0]
try:
ip = socket.gethostbyname(host)
logger.info(f"✅ DNS resolved: {host} -> {ip}")
except socket.gaierror as e:
logger.error(f"❌ DNS failed for {host}: {e}")
return False
test_url = f"{base_url}/api/v1/orderBookDetails?market_id={MARKET_ID}"
try:
response = requests.get(test_url, timeout=10)
logger.info(f"✅ HTTP test: {response.status_code} for {test_url}")
if response.status_code != 200:
logger.warning(f"⚠️ Market {MARKET} (id={MARKET_ID}) may not exist or ticker endpoint invalid. Returned: {response.status_code} {response.text[:200]}")
return True
except requests.exceptions.RequestException as e:
logger.error(f"❌ HTTP failed: {e}")
return False
# --- Test nonce endpoint ---
def test_nonce_endpoint():
logger.info("=== Testing nonce endpoint ===")
url = f"{BASE_URL}/api/v1/nextNonce"
params = {'account_index': ACCOUNT_INDEX, 'api_key_index': API_KEY_INDEX}
try:
r = requests.get(url, params=params, timeout=10)
logger.info(f"Status: {r.status_code}")
logger.info(f"Response: {r.text[:200]}")
if r.status_code == 200:
data = r.json()
logger.info(f"Nonce data: {data}")
except Exception as e:
logger.error(f"Nonce test failed: {e}")
# --- Helpers ---
def fetch_orderbook_details(base_url, market_id):
url = f"{base_url}/api/v1/orderBookDetails?market_id={market_id}"
try:
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
return resp.json()
else:
logger.warning(f"orderBookDetails HTTP {resp.status_code}: {resp.text[:200]}")
return None
except Exception as e:
logger.error(f"fetch_orderbook_details failed: {e}")
return None
def fetch_orderbook_parsed(base_url, market_id):
data = fetch_orderbook_details(base_url, market_id)
if not data:
return {"bids": [], "asks": [], "bestBid": "0", "bestAsk": "0", "price_decimals": 6, "size_decimals": 0}
details = data.get("order_book_details", [{}])[0]
bids = details.get("bids", [])
asks = details.get("asks", [])
if not bids or not asks:
last_trade_price = details.get("last_trade_price")
if last_trade_price:
lp = Decimal(str(last_trade_price))
bids = [[str(lp * Decimal("0.999999")), "1"]]
asks = [[str(lp * Decimal("1.000001")), "1"]]
bestBid = details.get("bestBid") or (bids[0][0] if bids else "0")
bestAsk = details.get("bestAsk") or (asks[0][0] if asks else "0")
price_decimals = int(details.get("supported_price_decimals", 6))
size_decimals = int(details.get("supported_size_decimals", 0))
return {"bids": bids, "asks": asks, "bestBid": str(bestBid), "bestAsk": str(bestAsk), "price_decimals": price_decimals, "size_decimals": size_decimals}
def fetch_open_orders_rest(base_url, account_index, market_id, auth):
url = f"{base_url}/api/v1/accountActiveOrders"
params = {"account_index": account_index, "market_id": market_id}
try:
resp = requests.get(url, params=params, timeout=10, headers={"accept": "application/json", "Authorization": auth})
if resp.status_code == 200:
data = resp.json()
orders = data.get("orders") or data.get("open_orders") or (data if isinstance(data, list) else [])
logger.info(f"Full open orders response: {json.dumps(orders)}")
return orders
else:
logger.warning(f"Open orders fetch failed: {resp.status_code} {resp.text[:200]}")
return []
except Exception as e:
logger.error(f"Error fetching open orders: {e}")
return []
def calculate_atr(base_url, market_id, period=ATR_PERIOD):
"""Calculate 1-minute ATR based on recent price data."""
try:
prices = []
for _ in range(period):
ob = fetch_orderbook_parsed(base_url, market_id)
if not ob.get("bids") or not ob.get("asks"):
logger.warning("No valid orderbook data for ATR calculation")
return Decimal("0")
mid_price = (Decimal(ob["bestBid"]) + Decimal(ob["bestAsk"])) / 2
prices.append(mid_price)
time.sleep(60 / period) # Simulate 1-minute intervals
if len(prices) < period:
logger.warning("Insufficient price data for ATR")
return Decimal("0")
tr_list = []
for i in range(1, len(prices)):
high = max(prices[i], prices[i-1])
low = min(prices[i], prices[i-1])
tr = high - low
tr_list.append(tr)
atr = sum(tr_list) / len(tr_list) if tr_list else Decimal("0")
logger.info(f"Calculated ATR: {atr}")
return atr
except Exception as e:
logger.error(f"ATR calculation failed: {e}")
return Decimal("0")
async def get_mid_price(public_api, market_id):
ob = fetch_orderbook_parsed(BASE_URL, market_id)
bids = ob.get("bids", [])
asks = ob.get("asks", [])
if not bids or not asks:
raise ValueError("No valid orderbook data")
best_bid = Decimal(str(bids[0][0]))
best_ask = Decimal(str(asks[0][0]))
return (best_bid + best_ask) / 2
# --- Place Orders ---
async def place_orders(signer, api, public_api, use_manual_nonce=False, nonce_manager=None):
try:
# Check volatility with ATR
atr = calculate_atr(BASE_URL, MARKET_ID)
if atr > ATR_THRESHOLD:
logger.info(f"Volatility too high (ATR={atr} > {ATR_THRESHOLD}), skipping order placement")
return
# Fetch nonce
if use_manual_nonce and nonce_manager:
nonce = nonce_manager.get_next_nonce()
else:
next_nonce_obj = await api.next_nonce(api_key_index=API_KEY_INDEX, account_index=ACCOUNT_INDEX)
nonce = int(next_nonce_obj.nonce)
# Generate auth token
auth, err = signer.create_auth_token_with_expiry(SignerClient.DEFAULT_10_MIN_AUTH_EXPIRY)
if err is not None:
logger.error(f"Auth error: {err}")
return
# Fetch open orders to determine market direction
open_orders = fetch_open_orders_rest(BASE_URL, ACCOUNT_INDEX, MARKET_ID, auth)
logger.info(f"Open orders found (REST): {len(open_orders)}")
# Check market direction based on open orders
has_open_ask = any(order.get("is_ask", 0) == 1 for order in open_orders)
has_open_bid = any(order.get("is_ask", 0) == 0 for order in open_orders)
# Adjust spreads dynamically
bid_spread_multiplier = 1
ask_spread_multiplier = 1
if has_open_ask:
logger.info("Detected open ask order, market moving down, doubling bid spread")
bid_spread_multiplier = 2
if not has_open_bid:
logger.info("Previous bid likely filled, increasing bid spread further")
bid_spread_multiplier = 4
if has_open_bid:
logger.info("Detected open bid order, market moving up, doubling ask spread")
ask_spread_multiplier = 2
if not has_open_ask:
logger.info("Previous ask likely filled, increasing ask spread further")
ask_spread_multiplier = 4
# Fetch orderbook and calculate prices
ob_parsed = fetch_orderbook_parsed(BASE_URL, MARKET_ID)
best_bid = Decimal(str(ob_parsed["bids"][0][0]))
best_ask = Decimal(str(ob_parsed["asks"][0][0]))
mid = (best_bid + best_ask) / 2
logger.info(f"Mid price: {mid}")
DESIRED_USD = Decimal("20")
usd_based_size = (DESIRED_USD / mid).quantize(Decimal("0.0000001"), rounding="ROUND_HALF_UP")
order_size = max(Decimal("0.005"), usd_based_size) # Enforce min 0.01 AVAX
# Adjust spreads based on market direction
bid_spread = SPREAD * bid_spread_multiplier
ask_spread = SPREAD * ask_spread_multiplier
buy_price = (mid * (1 - bid_spread / 2)).quantize(Decimal("0.0000001"))
sell_price = (mid * (1 + ask_spread / 2)).quantize(Decimal("0.0000001"))
# Calculate stop-loss prices
stop_loss_buy = (buy_price * (1 + STOP_LOSS_PNL / mid)).quantize(Decimal("0.0000001")) # For buy: price goes up
stop_loss_sell = (sell_price * (1 - STOP_LOSS_PNL / mid)).quantize(Decimal("0.0000001")) # For sell: price goes down
logger.info(f"Buy price: {buy_price}, Sell price: {sell_price}, Bid spread multiplier: {bid_spread_multiplier}, Ask spread multiplier: {ask_spread_multiplier}")
logger.info(f"Stop-loss buy: {stop_loss_buy}, Stop-loss sell: {stop_loss_sell}")
price_decimals = int(ob_parsed.get("price_decimals", 6))
size_decimals = int(ob_parsed.get("size_decimals", 0))
scaled_buy_price = int((buy_price * (10 ** price_decimals)).to_integral_value(rounding="ROUND_HALF_UP"))
scaled_sell_price = int((sell_price * (10 ** price_decimals)).to_integral_value(rounding="ROUND_HALF_UP"))
scaled_stop_loss_buy = int((stop_loss_buy * (10 ** price_decimals)).to_integral_value(rounding="ROUND_HALF_UP"))
scaled_stop_loss_sell = int((stop_loss_sell * (10 ** price_decimals)).to_integral_value(rounding="ROUND_HALF_UP"))
scaled_size = int((order_size * (10 ** size_decimals)).to_integral_value(rounding="ROUND_HALF_UP"))
base_client_order_index = int(pytime.time() * 1000)
next_client_idx = 0
expiration_time_ms = int(time.time() * 1000) + (5 * 60 * 1000)
# Place BUY limit order
tx_info_buy, err_buy = signer.sign_create_order(
int(MARKET_ID),
int(base_client_order_index + next_client_idx),
int(scaled_size),
int(scaled_buy_price),
int(0), # is_ask
int(0), # LIMIT
int(SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME),
int(0), # reduce_only
int(0), # trigger_price
int(SignerClient.DEFAULT_28_DAY_ORDER_EXPIRY),
int(nonce)
)
if err_buy:
logger.error(f"Sign buy error: {err_buy}")
else:
tx_info_buy_str = tx_info_buy if isinstance(tx_info_buy, str) else json.dumps(tx_info_buy)
try:
response = await api.send_tx(tx_type=StrictInt(signer.TX_TYPE_CREATE_ORDER), tx_info=tx_info_buy_str)
logger.info(f"✅ Placed BUY {order_size} at {buy_price}. Send tx response: {response}")
except Exception as e:
logger.error(f"Send tx error for BUY: {e}")
nonce += 1
next_client_idx += 1
# Place BUY stop-loss order (sell if price rises to stop_loss_buy)
tx_info_buy_stop, err_buy_stop = signer.sign_create_order(
int(MARKET_ID),
int(base_client_order_index + next_client_idx),
int(scaled_size),
int(scaled_stop_loss_buy),
int(1), # is_ask (sell to close buy)
int(2), # STOP
int(SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME),
int(0), # reduce_only
int(scaled_stop_loss_buy), # trigger_price
int(SignerClient.DEFAULT_28_DAY_ORDER_EXPIRY),
int(nonce)
)
if err_buy_stop:
logger.error(f"Sign buy stop-loss error: {err_buy_stop}")
else:
tx_info_buy_stop_str = tx_info_buy_stop if isinstance(tx_info_buy_stop, str) else json.dumps(tx_info_buy_stop)
try:
response = await api.send_tx(tx_type=StrictInt(signer.TX_TYPE_CREATE_ORDER), tx_info=tx_info_buy_stop_str)
logger.info(f"✅ Placed BUY stop-loss {order_size} at {stop_loss_buy}. Send tx response: {response}")
except Exception as e:
logger.error(f"Send tx error for BUY stop-loss: {e}")
nonce += 1
next_client_idx += 1
# Place SELL limit order
tx_info_sell, err_sell = signer.sign_create_order(
int(MARKET_ID),
int(base_client_order_index + next_client_idx),
int(scaled_size),
int(scaled_sell_price),
int(1), # is_ask
int(0), # LIMIT
int(SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME),
int(0), # reduce_only
int(0), # trigger_price
int(SignerClient.DEFAULT_28_DAY_ORDER_EXPIRY),
int(nonce)
)
if err_sell:
logger.error(f"Sign sell error: {err_sell}")
else:
tx_info_sell_str = tx_info_sell if isinstance(tx_info_sell, str) else json.dumps(tx_info_sell)
try:
response = await api.send_tx(tx_type=StrictInt(signer.TX_TYPE_CREATE_ORDER), tx_info=tx_info_sell_str)
logger.info(f"✅ Placed SELL {order_size} at {sell_price}. Send tx response: {response}")
except Exception as e:
logger.error(f"Send tx error for SELL: {e}")
nonce += 1
next_client_idx += 1
# Place SELL stop-loss order (buy if price falls to stop_loss_sell)
tx_info_sell_stop, err_sell_stop = signer.sign_create_order(
int(MARKET_ID),
int(base_client_order_index + next_client_idx),
int(scaled_size),
int(scaled_stop_loss_sell),
int(0), # is_ask (buy to close sell)
int(2), # STOP
int(SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME),
int(0), # reduce_only
int(scaled_stop_loss_sell), # trigger_price
int(SignerClient.DEFAULT_28_DAY_ORDER_EXPIRY),
int(nonce)
)
if err_sell_stop:
logger.error(f"Sign sell stop-loss error: {err_sell_stop}")
else:
tx_info_sell_stop_str = tx_info_sell_stop if isinstance(tx_info_sell_stop, str) else json.dumps(tx_info_sell_stop)
try:
response = await api.send_tx(tx_type=StrictInt(signer.TX_TYPE_CREATE_ORDER), tx_info=tx_info_sell_stop_str)
logger.info(f"✅ Placed SELL stop-loss {order_size} at {stop_loss_sell}. Send tx response: {response}")
except Exception as e:
logger.error(f"Send tx error for SELL stop-loss: {e}")
nonce += 1
# Log open orders to verify new orders were added
open_orders_after_place = fetch_open_orders_rest(BASE_URL, ACCOUNT_INDEX, MARKET_ID, auth)
logger.info(f"Open orders after placement: {len(open_orders_after_place)}")
except Exception as e:
logger.error(f"Error in MM loop: {e}")
logger.error(traceback.format_exc())
# --- Main ---
async def main():
if not PRIVATE_KEY:
logger.error("PRIVATE_KEY not set")
return
use_manual_nonce = False
nonce_manager = None
if use_manual_nonce:
nonce_manager = ManualNonceManager(BASE_URL)
try:
signer = SignerClient(private_key=PRIVATE_KEY, url=BASE_URL, api_key_index=API_KEY_INDEX, account_index=ACCOUNT_INDEX)
api = TransactionApi()
public_api = ApiClient()
logger.info(f"Starting MM bot on {MARKET} (id={MARKET_ID}) with {BASE_URL}")
except Exception as e:
logger.error(f"Client init failed: {e}. Falling back to manual nonce.")
use_manual_nonce = True
nonce_manager = ManualNonceManager(BASE_URL)
signer = SignerClient(private_key=PRIVATE_KEY, url=BASE_URL, api_key_index=API_KEY_INDEX, account_index=ACCOUNT_INDEX)
api = TransactionApi()
public_api = ApiClient()
while True:
await place_orders(signer, api, public_api, use_manual_nonce, nonce_manager)
await asyncio.sleep(REFRESH_INTERVAL)
if __name__ == "__main__":
if not validate_connectivity(BASE_URL):
logger.error("❌ Connectivity validation failed. Exiting.")
exit(1)
test_nonce_endpoint()
asyncio.run(main())
```