JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Untitled Page | JustPaste.app
4 days ago4 views
👨‍💻Programming
import shutil
import os
import re
import time
import threading
import urllib.parse
from datetime import datetime, timezone

import requests
import socketio
from cryptography.hazmat.primitives.asymmetric import ed25519


# ============================================================
# CONFIGURATION
# ============================================================

INTERVAL = "5"          # 5-minute candles

# ================================================================
# MASTER SAFETY SWITCH
#
#   False -> paper trading only. Signals, SL, exits, PnL are all
#            simulated in memory. NOTHING is sent to CoinSwitch.
#   True  -> REAL orders are placed with REAL money on your
#            CoinSwitch PRO futures account (fixed 10x leverage,
#            MARKET orders). Do not flip this to True until you
#            have watched the bot run in paper mode and are
#            comfortable with its behaviour.
# ================================================================
ORDERS_ENABLED = False

# Leverage applied to every symbol before its first trade (fixed,
# per your instruction). Must be <= the symbol's max_leverage -
# since the watchlist only includes symbols with max_leverage
# above MIN_LEVERAGE_FILTER (10), this fixed value is always valid.
TRADE_LEVERAGE = 10

# Maximum allowed loss on trading capital when the fixed SL is hit.
# Raw price risk is multiplied by TRADE_LEVERAGE. >30% is rejected.
MAX_SL_RISK_PCT = 30.0

PRINT_INTERVAL = 2.0

# --------------------------------------------------------------
# WATCHLIST (Top Gainers / Top Losers)
# --------------------------------------------------------------

TOP_N_GAINERS = 20
TOP_N_LOSERS = 20

# Only trade instruments whose max allowed leverage is ABOVE this.
# (Read from Get Instrument Info's max_leverage field per symbol.)
MIN_LEVERAGE_FILTER = 10.0

# How often (seconds) the gainers/losers watchlist is re-scanned.
WATCHLIST_REFRESH_SECONDS = 4 * 60 * 60   # 4 hours


# --------------------------------------------------------------
# CAPITAL ALLOCATION (paper-trading sizing - ORDERS_ENABLED is
# False, so no real orders are placed, but position sizing is
# simulated so PnL in USDT terms makes sense).
#
# Quantity per trade is NOT a fixed USDT slice anymore - each
# trade uses the exchange's MINIMUM allowed order quantity for
# that symbol (Get Instrument Info -> min_base_quantity). The
# capital numbers below only act as a safety cap: total notional
# value across all open positions is never allowed to exceed
# MAX_ALLOCATION_PCT of TOTAL_CAPITAL, and no more than
# MAX_CONCURRENT_POSITIONS trades run at once.
# --------------------------------------------------------------

TOTAL_CAPITAL = 10000.0        # <-- set this to your real capital
MAX_ALLOCATION_PCT = 50.0      # never deploy more than this % of capital at once
MAX_CONCURRENT_POSITIONS = 1   # only ONE trade open at a time (for now)

MAX_DEPLOYABLE_CAPITAL = TOTAL_CAPITAL * (MAX_ALLOCATION_PCT / 100.0)

# --------------------------------------------------------------
# WEBSOCKET (candles only - ticker/live price comes from REST)
# --------------------------------------------------------------

WS_URL = "wss://ws.coinswitch.co/"
NAMESPACE = "/exchange_2"
SOCKETIO_PATH = "/pro/realtime-rates-socket/futures/exchange_2"

# --------------------------------------------------------------
# REST API
# --------------------------------------------------------------

BASE_URL = "https://coinswitch.co"

# Fill these in with your own CoinSwitch PRO API key pair
# (Profile -> API Trading on CoinSwitch PRO). Both are hex strings.
API_KEY = os.environ.get("COINSWITCH_API_KEY", "")
SECRET_KEY = os.environ.get("COINSWITCH_SECRET_KEY", "")

# How often (seconds) to poll the REST all-pairs ticker for live
# prices of every symbol in the watchlist.
TICKER_POLL_SECONDS = 2.0


# ============================================================
# WEBSOCKET CLIENT
# ============================================================

sio = socketio.Client(
    reconnection=True,
    reconnection_attempts=0,
    reconnection_delay=2,
    reconnection_delay_max=10,
)


# ============================================================
# SHARED STATE
# ============================================================

state_lock = threading.Lock()

ws_connected = False

# The symbols we currently want NEW entries on (refreshed every
# WATCHLIST_REFRESH_SECONDS).
watchlist = set()

# Every symbol we've ever subscribed a KLine stream for. This is a
# superset of `watchlist` - a symbol stays here (and keeps getting
# its candles/price updated) even after it drops out of the
# watchlist, for as long as it still has an open position, so an
# open trade is never abandoned mid-flight.
subscribed_symbols = set()

# Per-symbol state. Keys are symbol strings (e.g. "BTCUSDT").
# See make_symbol_state() for the shape of each value.
symbol_states = {}

# Simple trade log (kept in memory since orders are disabled).
# Each entry also carries a "symbol" key.
trade_log = []

last_watchlist_refresh = None

# Per-symbol trading rules from Get Instrument Info: max_leverage
# and min_base_quantity. Refreshed alongside the watchlist.
instrument_info = {}

# Symbols for which we've successfully set TRADE_LEVERAGE (only
# relevant when ORDERS_ENABLED is True - leverage must be set
# before the first order on a symbol).
leverage_set_symbols = set()


# ============================================================
# HELPERS
# ============================================================

def to_float(value):
    try:
        if value is None:
            return None
        return float(value)
    except Exception:
        return None


def to_int(value):
    try:
        if value is None:
            return None
        return int(value)
    except Exception:
        return None


def utc_string(timestamp_ms):
    if timestamp_ms is None:
        return "N/A"

    try:
        dt = datetime.fromtimestamp(
            timestamp_ms / 1000.0,
            tz=timezone.utc,
        )

        return dt.strftime("%Y-%m-%d %H:%M:%S UTC")

    except Exception:
        return "N/A"


def now_string():
    return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")


def make_symbol_state():
    """
    Fresh per-symbol state dict. Every symbol we track gets one
    of these (candle history, signal search state, position).
    """

    return {
        "current_candle": None,
        "previous_closed_candle": None,

        "live_price": None,
        "previous_live_price": None,

        "signal": "WAIT",
        "entry_price": None,
        "stop_loss": None,
        "signal_candle_start": None,
        "signal_triggered": False,

        "in_position": False,
        "position_side": None,       # "BUY" or "SELL"
        "position_entry": None,
        "position_sl": None,
        "position_size": None,       # USDT notional (qty * entry price)
        "position_qty": None,        # base-asset quantity (min_qty for the symbol)
        "position_open_time": None,
    }


def ensure_symbol_state(symbol):
    """
    Must be called under state_lock. Creates a fresh state entry
    for `symbol` if one doesn't already exist.
    """

    if symbol not in symbol_states:
        symbol_states[symbol] = make_symbol_state()


# ============================================================
# REST API (Ed25519-signed requests)
# ============================================================

def sign_request(method, path, params=None):
    """
    Build the headers and final URL path for an authenticated
    CoinSwitch request (Ed25519 signing), per CoinSwitch PRO's
    API Trading docs.
    """

    method = method.upper()

    if params:
        sep = "&" if "?" in path else "?"
        path = path + sep + urllib.parse.urlencode(params)

    decoded_path = urllib.parse.unquote_plus(path)

    epoch = str(int(time.time() * 1000))

    message = method + decoded_path + epoch

    secret = ed25519.Ed25519PrivateKey.from_private_bytes(
        bytes.fromhex(SECRET_KEY)
    )

    signature = secret.sign(message.encode("utf-8")).hex()

    headers = {
        "Content-Type": "application/json",
        "X-AUTH-APIKEY": API_KEY,
        "X-AUTH-SIGNATURE": signature,
        "X-AUTH-EPOCH": epoch,
    }

    return headers, decoded_path


def fetch_all_pairs_ticker():
    """
    GET /trade/api/v2/futures/all-pairs/ticker

    Returns a dict: { "BTCUSDT": {"price": .., "bid": .., "ask": ..,
    "mark": .., "timestamp": .., "pct24h": ..}, ... } on success,
    or None on any failure. Never raises.
    """

    try:

        headers, path = sign_request(
            "GET",
            "/trade/api/v2/futures/all-pairs/ticker",
            params={"exchange": "EXCHANGE_2"},
        )

        response = requests.get(
            BASE_URL + path,
            headers=headers,
            timeout=10,
        )

        response.raise_for_status()

        payload = response.json()

        raw = payload.get("data")

        if not isinstance(raw, dict):
            return None

        result = {}

        for symbol, ticker in raw.items():

            if not isinstance(ticker, dict):
                continue

            price = to_float(ticker.get("last_price"))

            if price is None:
                continue

            result[symbol] = {
                "price": price,
                "bid": to_float(ticker.get("best_bid_price")),
                "ask": to_float(ticker.get("best_ask_price")),
                "mark": to_float(ticker.get("mark_price")),
                "timestamp": to_int(ticker.get("timestamp")),
                "pct24h": to_float(ticker.get("price_24h_pcnt")),
            }

        return result

    except Exception as e:

        print(f"[REST All-Pairs Ticker Error] {e}")
        return None


def fetch_futures_balance():
    """
    GET /trade/api/v2/futures/wallet_balance

    Returns total available USDT futures balance, or None on failure.
    Read-only request. Never places or modifies any order.
    """

    try:

        headers, path = sign_request(
            "GET",
            "/trade/api/v2/futures/wallet_balance",
        )

        response = requests.get(
            BASE_URL + path,
            headers=headers,
            timeout=10,
        )

        response.raise_for_status()

        payload = response.json()

        data = payload.get("data", {})
        balances = data.get("base_asset_balances", [])

        if not isinstance(balances, list):
            return None

        for asset in balances:

            if not isinstance(asset, dict):
                continue

            if asset.get("base_asset") != "USDT":
                continue

            balance_data = asset.get("balances", {})

            if not isinstance(balance_data, dict):
                return None

            return to_float(
                balance_data.get("total_available_balance")
            )

        return None

    except Exception as e:

        print(f"[REST Futures Balance Error] {e}")
        return None


def fetch_instrument_info():
    """
    GET /trade/api/v2/futures/instrument_info

    Returns a dict: { "BTCUSDT": {"max_leverage": .., "min_qty": ..}, ... }
    on success, or None on any failure. Never raises.
    """

    try:

        headers, path = sign_request(
            "GET",
            "/trade/api/v2/futures/instrument_info",
            params={"exchange": "EXCHANGE_2"},
        )

        response = requests.get(
            BASE_URL + path,
            headers=headers,
            timeout=10,
        )

        response.raise_for_status()

        payload = response.json()

        raw = payload.get("data")

        if not isinstance(raw, dict):
            return None

        result = {}

        for symbol, info in raw.items():

            if not isinstance(info, dict):
                continue

            max_leverage = to_float(info.get("max_leverage"))
            min_qty = to_float(info.get("min_base_quantity"))

            if max_leverage is None or min_qty is None:
                continue

            result[symbol] = {
                "max_leverage": max_leverage,
                "min_qty": min_qty,
            }

        return result

    except Exception as e:

        print(f"[REST Instrument Info Error] {e}")
        return None


# ============================================================
# REAL ORDER PLACEMENT (only actually called when ORDERS_ENABLED)
# ============================================================

def set_symbol_leverage(symbol):
    """
    POST /trade/api/v2/futures/leverage

    Sets TRADE_LEVERAGE for `symbol`. Must be called BEFORE the
    first order on that symbol (leverage can't be changed while
    there's an open position or open order on it). Returns True
    on success, False on failure. Never raises.
    """

    try:

        headers, path = sign_request(
            "POST", "/trade/api/v2/futures/leverage"
        )

        body = {
            "symbol": symbol,
            "exchange": "EXCHANGE_2",
            "leverage": TRADE_LEVERAGE,
        }

        response = requests.post(
            BASE_URL + path,
            headers=headers,
            json=body,
            timeout=10,
        )

        response.raise_for_status()

        print(f"[Leverage] {symbol} set to {TRADE_LEVERAGE}x")
        return True

    except Exception as e:

        print(f"[Leverage Error] ({symbol}) {e}")
        return False


def place_market_order(symbol, side, quantity, reduce_only=False):
    """
    POST /trade/api/v2/futures/order (MARKET order).

    side        — "BUY" or "SELL"
    quantity    — base-asset quantity
    reduce_only — True when this order is meant to CLOSE an
                  existing position (never opens a new/opposite one)

    Returns the parsed response dict on success, or None on any
    failure. Never raises.
    """

    try:

        headers, path = sign_request(
            "POST", "/trade/api/v2/futures/order"
        )

        body = {
            "exchange": "EXCHANGE_2",
            "symbol": symbol,
            "side": side,
            "order_type": "MARKET",
            "quantity": quantity,
        }

        if reduce_only:
            body["reduce_only"] = True

        response = requests.post(
            BASE_URL + path,
            headers=headers,
            json=body,
            timeout=10,
        )

        response.raise_for_status()

        data = response.json()

        print(f"[Order] {symbol} {side} qty={quantity} reduce_only={reduce_only} -> {data}")

        return data

    except Exception as e:

        print(f"[Order Error] ({symbol} {side} qty={quantity}) {e}")
        return None


# ============================================================
# HISTORICAL KLINES (REST) - used to seed a newly-added symbol's
# "previous closed candle" immediately, instead of waiting for
# it to arrive organically over the live WebSocket stream.
# ============================================================

def fetch_recent_klines(symbol, limit=3):
    """
    GET /trade/api/v2/futures/klines

    Returns a list of candle dicts (sorted oldest -> newest), each
    with: start_time, close_time, open, high, low, close, volume.
    Returns None on any failure. Never raises.
    """

    try:

        headers, path = sign_request(
            "GET",
            "/trade/api/v2/futures/klines",
            params={
                "symbol": symbol.lower(),
                "exchange": "EXCHANGE_2",
                "interval": INTERVAL,
                "limit": limit,
            },
        )

        response = requests.get(
            BASE_URL + path,
            headers=headers,
            timeout=10,
        )

        response.raise_for_status()

        payload = response.json()

        raw = payload.get("data")

        if not isinstance(raw, list):
            return None

        candles = []

        for row in raw:

            start_time = to_int(row.get("start_time"))
            close_time = to_int(row.get("close_time"))
            o = to_float(row.get("o"))
            h = to_float(row.get("h"))
            l = to_float(row.get("l"))
            c = to_float(row.get("c"))
            v = to_float(row.get("volume"))

            if start_time is None or o is None or h is None or l is None or c is None:
                continue

            candles.append(
                {
                    "start_time": start_time,
                    "close_time": close_time,
                    "open": o,
                    "high": h,
                    "low": l,
                    "close": c,
                    "volume": v,
                }
            )

        candles.sort(key=lambda x: x["start_time"])

        return candles

    except Exception as e:

        print(f"[REST Klines Error] ({symbol}) {e}")
        return None


def seed_symbol_history(symbol):
    """
    Fetches the last few candles for `symbol` via REST and, if the
    symbol doesn't already have candle data (from the live
    WebSocket stream), seeds its previous_closed_candle (and, if
    available, current_candle too) so the strategy can evaluate
    signals immediately instead of waiting 5-10 minutes for two
    live candles to pass.
    """

    candles = fetch_recent_klines(symbol, limit=3)

    if not candles:
        return

    now_ms = int(time.time() * 1000)

    # A candle counts as fully closed only if its close_time has
    # actually passed - never trust the last row blindly, since
    # some APIs include the still-forming candle as the last row.
    closed = [
        c for c in candles
        if c["close_time"] is not None and c["close_time"] <= now_ms
    ]

    if not closed:
        return

    previous = closed[-1]

    # Whatever candle (if any) starts after `previous` is the
    # current, still-forming candle.
    current_candidate = None

    for c in candles:
        if c["start_time"] > previous["start_time"]:
            current_candidate = c
            break

    def strip(c):
        return {
            "start_time": c["start_time"],
            "open": c["open"],
            "high": c["high"],
            "low": c["low"],
            "close": c["close"],
            "volume": c["volume"],
        }

    with state_lock:

        if symbol not in symbol_states:
            return

        state = symbol_states[symbol]

        # Only seed if the live WebSocket hasn't already populated
        # this - never overwrite live data with a stale REST call.
        if state["previous_closed_candle"] is None:
            state["previous_closed_candle"] = strip(previous)

        if state["current_candle"] is None and current_candidate is not None:
            state["current_candle"] = strip(current_candidate)

    print(
        f"[Seed] {symbol}: previous candle loaded from REST "
        f"(O:{previous['open']:.6f} H:{previous['high']:.6f} "
        f"L:{previous['low']:.6f} C:{previous['close']:.6f})"
    )


# ============================================================
# WATCHLIST (Top Gainers / Top Losers)
# ============================================================

def compute_watchlist(all_ticker_data, instrument_data):
    """
    Given the dicts returned by fetch_all_pairs_ticker() and
    fetch_instrument_info(), first filters to symbols whose

    max_leverage is ABOVE MIN_LEVERAGE_FILTER, then picks the
    top TOP_N_GAINERS by 24h % change and the top TOP_N_LOSERS by
    (most negative) 24h % change from that eligible set. Returns a list of symbols
    (up to TOP_N_GAINERS + TOP_N_LOSERS, no duplicates).
    """

    ranked = [
        (symbol, data["pct24h"])
        for symbol, data in all_ticker_data.items()
        if data.get("pct24h") is not None
        and instrument_data.get(symbol, {}).get("max_leverage", 0)
        > MIN_LEVERAGE_FILTER
    ]

    if not ranked:
        return [], [], []

    ga

⚠️Content was pasted as plain text and auto-formatted as a code block. Use the Code Block button in the editor for proper formatting.

← Back to timeline