---
# System prepended metadata

title: import asyncioimport loggingim

---

```
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())
```