29 Commits

Author SHA1 Message Date
Nawaz Haider ef4cb47d06 Add update_signed_orders_cache method to OrderBook for managing signed orders 2026-01-15 19:57:59 +06:00
Nawaz Haider 5657b087c5 Reduce sleep duration in inventory updater thread from 2 seconds to 1 second 2026-01-15 19:37:36 +06:00
Nawaz Haider d7e2f8d04f Remove debug print statement from get_inventory function 2026-01-15 19:34:17 +06:00
Nawaz Haider 02bb3eefee Add inventory management with configurable limits and updater thread 2026-01-15 19:26:51 +06:00
Nawaz Haider e88752cb23 Refactor trading logic to simplify conditions for order placement 2026-01-15 18:23:14 +06:00
Nawaz Haider fa31c52cc3 Enhance trading logic to include time-based condition for trade execution 2026-01-15 16:43:36 +06:00
Nawaz Haider c1150f927a Fix trading logic conditions for order placement based on market trend direction 2026-01-15 16:32:02 +06:00
Nawaz Haider c97f96765c Fix trading logic conditions for order placement based on market trends 2026-01-15 16:24:53 +06:00
Nawaz Haider 3eeea819a9 Add nohup command to run merger.py in background 2026-01-15 16:13:30 +06:00
Nawaz Haider 768ec1358c Update trading parameters for improved strategy performance 2026-01-15 16:12:31 +06:00
Nawaz Haider bbfb33829e Remove test.py file containing unused asyncio test code 2026-01-14 13:13:53 +06:00
Nawaz Haider 7508654d00 Add Safe contract ABI and implement token merging functionality
- Introduced `safeAbi.py` containing the ABI for the Safe contract, defining events and functions for managing ownership and transactions.
- Created `merger.py` to handle merging conditional tokens back to USDC, including logic for determining merge amounts, signing transactions, and executing them through the Safe contract.
- Integrated environment variable loading for sensitive data and RPC connection setup.
- Implemented asynchronous polling for mergeable positions from the Polymarket API, with error handling and transaction receipt verification.
2026-01-14 13:12:37 +06:00
Nawaz Haider 5a579d49c0 Refactor main function and related methods to remove async/await, replacing with synchronous calls for improved performance and simplicity 2026-01-12 21:43:00 +06:00
Nawaz Haider da9244eabd Remove unused requests session setup from main.py 2026-01-12 21:36:00 +06:00
Nawaz Haider 7abcd3642c Refactor create_signed_orders_cache to be synchronous and remove asyncio task creation 2026-01-12 21:19:23 +06:00
Nawaz Haider 25d7e2a2d4 Refactor order placement to use ThreadPoolExecutor for synchronous execution and improve logging of order IDs 2026-01-12 21:12:31 +06:00
Nawaz Haider fd0e2b31b1 Remove unnecessary async task for caching token trading info in main function 2026-01-12 20:41:05 +06:00
Nawaz Haider ca8e167275 Remove duplicate call to create_signed_orders_cache in OrderBook start method 2026-01-12 20:39:43 +06:00
Nawaz Haider 351a76a308 Add MIN_DELAY_BETWEEN_TRADES_SECONDS to config and main logic for trade pacing 2026-01-12 16:15:16 +06:00
Nawaz Haider b4c3eb90c9 Update trading configuration and enhance order placement logic with signed orders caching 2026-01-12 15:36:24 +06:00
Nawaz Haider 430f2e9ad0 Fix typo in variable name for micro vs mid basis points calculation 2026-01-10 19:12:46 +06:00
Nawaz Haider 32518bfceb Update trading thresholds in config and main logic for improved order placement 2026-01-10 19:10:40 +06:00
Nawaz Haider e0cd82553a Add market data logging to UP and DOWN order placements 2026-01-10 18:59:36 +06:00
Nawaz Haider 1de0ebd22e Refactor place_limit_order to remove expiration handling and simplify order placement 2026-01-06 18:56:29 +06:00
Nawaz Haider bf1c3515c0 Remove order_listener.py 2026-01-04 22:01:35 +06:00
Nawaz Haider a67afb031c Update README.md to reflect recent changes and improvements 2026-01-04 21:42:28 +06:00
Nawaz Haider 145fc5e6f4 Merge branch 'main' of https://github.com/nawaz0x1/py_polymarket_hft_mm 2026-01-04 21:20:27 +06:00
Nawaz Haider a31995f362 Remove in-memory database references and related setup from scripts 2026-01-04 21:17:25 +06:00
Nawaz Haider 98fe566336 Remove in-memory database implementation and associated utility functions 2026-01-04 21:14:55 +06:00
16 changed files with 2271 additions and 421 deletions
BIN
View File
Binary file not shown.
View File
+721
View File
@@ -0,0 +1,721 @@
ctf_abi = [
{
"constant":True,
"inputs":[
{
"name":"owner",
"type":"address"
},
{
"name":"id",
"type":"uint256"
}
],
"name":"balanceOf",
"outputs":[
{
"name":"",
"type":"uint256"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"interfaceId",
"type":"bytes4"
}
],
"name":"supportsInterface",
"outputs":[
{
"name":"",
"type":"bool"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"",
"type":"bytes32"
},
{
"name":"",
"type":"uint256"
}
],
"name":"payoutNumerators",
"outputs":[
{
"name":"",
"type":"uint256"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"from",
"type":"address"
},
{
"name":"to",
"type":"address"
},
{
"name":"ids",
"type":"uint256[]"
},
{
"name":"values",
"type":"uint256[]"
},
{
"name":"data",
"type":"bytes"
}
],
"name":"safeBatchTransferFrom",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"owners",
"type":"address[]"
},
{
"name":"ids",
"type":"uint256[]"
}
],
"name":"balanceOfBatch",
"outputs":[
{
"name":"",
"type":"uint256[]"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"operator",
"type":"address"
},
{
"name":"approved",
"type":"bool"
}
],
"name":"setApprovalForAll",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"",
"type":"bytes32"
}
],
"name":"payoutDenominator",
"outputs":[
{
"name":"",
"type":"uint256"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"owner",
"type":"address"
},
{
"name":"operator",
"type":"address"
}
],
"name":"isApprovedForAll",
"outputs":[
{
"name":"",
"type":"bool"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"from",
"type":"address"
},
{
"name":"to",
"type":"address"
},
{
"name":"id",
"type":"uint256"
},
{
"name":"value",
"type":"uint256"
},
{
"name":"data",
"type":"bytes"
}
],
"name":"safeTransferFrom",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"conditionId",
"type":"bytes32"
},
{
"indexed":True,
"name":"oracle",
"type":"address"
},
{
"indexed":True,
"name":"questionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"outcomeSlotCount",
"type":"uint256"
}
],
"name":"ConditionPreparation",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"conditionId",
"type":"bytes32"
},
{
"indexed":True,
"name":"oracle",
"type":"address"
},
{
"indexed":True,
"name":"questionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"outcomeSlotCount",
"type":"uint256"
},
{
"indexed":False,
"name":"payoutNumerators",
"type":"uint256[]"
}
],
"name":"ConditionResolution",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"stakeholder",
"type":"address"
},
{
"indexed":False,
"name":"collateralToken",
"type":"address"
},
{
"indexed":True,
"name":"parentCollectionId",
"type":"bytes32"
},
{
"indexed":True,
"name":"conditionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"partition",
"type":"uint256[]"
},
{
"indexed":False,
"name":"amount",
"type":"uint256"
}
],
"name":"PositionSplit",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"stakeholder",
"type":"address"
},
{
"indexed":False,
"name":"collateralToken",
"type":"address"
},
{
"indexed":True,
"name":"parentCollectionId",
"type":"bytes32"
},
{
"indexed":True,
"name":"conditionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"partition",
"type":"uint256[]"
},
{
"indexed":False,
"name":"amount",
"type":"uint256"
}
],
"name":"PositionsMerge",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"redeemer",
"type":"address"
},
{
"indexed":True,
"name":"collateralToken",
"type":"address"
},
{
"indexed":True,
"name":"parentCollectionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"conditionId",
"type":"bytes32"
},
{
"indexed":False,
"name":"indexSets",
"type":"uint256[]"
},
{
"indexed":False,
"name":"payout",
"type":"uint256"
}
],
"name":"PayoutRedemption",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"operator",
"type":"address"
},
{
"indexed":True,
"name":"from",
"type":"address"
},
{
"indexed":True,
"name":"to",
"type":"address"
},
{
"indexed":False,
"name":"id",
"type":"uint256"
},
{
"indexed":False,
"name":"value",
"type":"uint256"
}
],
"name":"TransferSingle",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"operator",
"type":"address"
},
{
"indexed":True,
"name":"from",
"type":"address"
},
{
"indexed":True,
"name":"to",
"type":"address"
},
{
"indexed":False,
"name":"ids",
"type":"uint256[]"
},
{
"indexed":False,
"name":"values",
"type":"uint256[]"
}
],
"name":"TransferBatch",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":True,
"name":"owner",
"type":"address"
},
{
"indexed":True,
"name":"operator",
"type":"address"
},
{
"indexed":False,
"name":"approved",
"type":"bool"
}
],
"name":"ApprovalForAll",
"type":"event"
},
{
"anonymous":False,
"inputs":[
{
"indexed":False,
"name":"value",
"type":"string"
},
{
"indexed":True,
"name":"id",
"type":"uint256"
}
],
"name":"URI",
"type":"event"
},
{
"constant":False,
"inputs":[
{
"name":"oracle",
"type":"address"
},
{
"name":"questionId",
"type":"bytes32"
},
{
"name":"outcomeSlotCount",
"type":"uint256"
}
],
"name":"prepareCondition",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"questionId",
"type":"bytes32"
},
{
"name":"payouts",
"type":"uint256[]"
}
],
"name":"reportPayouts",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"collateralToken",
"type":"address"
},
{
"name":"parentCollectionId",
"type":"bytes32"
},
{
"name":"conditionId",
"type":"bytes32"
},
{
"name":"partition",
"type":"uint256[]"
},
{
"name":"amount",
"type":"uint256"
}
],
"name":"splitPosition",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"collateralToken",
"type":"address"
},
{
"name":"parentCollectionId",
"type":"bytes32"
},
{
"name":"conditionId",
"type":"bytes32"
},
{
"name":"partition",
"type":"uint256[]"
},
{
"name":"amount",
"type":"uint256"
}
],
"name":"mergePositions",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":False,
"inputs":[
{
"name":"collateralToken",
"type":"address"
},
{
"name":"parentCollectionId",
"type":"bytes32"
},
{
"name":"conditionId",
"type":"bytes32"
},
{
"name":"indexSets",
"type":"uint256[]"
}
],
"name":"redeemPositions",
"outputs":[
],
"payable":False,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"conditionId",
"type":"bytes32"
}
],
"name":"getOutcomeSlotCount",
"outputs":[
{
"name":"",
"type":"uint256"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"oracle",
"type":"address"
},
{
"name":"questionId",
"type":"bytes32"
},
{
"name":"outcomeSlotCount",
"type":"uint256"
}
],
"name":"getConditionId",
"outputs":[
{
"name":"",
"type":"bytes32"
}
],
"payable":False,
"stateMutability":"pure",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"parentCollectionId",
"type":"bytes32"
},
{
"name":"conditionId",
"type":"bytes32"
},
{
"name":"indexSet",
"type":"uint256"
}
],
"name":"getCollectionId",
"outputs":[
{
"name":"",
"type":"bytes32"
}
],
"payable":False,
"stateMutability":"view",
"type":"function"
},
{
"constant":True,
"inputs":[
{
"name":"collateralToken",
"type":"address"
},
{
"name":"collectionId",
"type":"bytes32"
}
],
"name":"getPositionId",
"outputs":[
{
"name":"",
"type":"uint256"
}
],
"payable":False,
"stateMutability":"pure",
"type":"function"
}
]
+1138
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -5,9 +5,12 @@ POLYMARKET_WS_MARKET_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market
POLYMARKET_WS_USER_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
CHAIN_ID = 137
REQUEST_TIMEOUT = 5
PROFIT_MARGIN = 0.02
PROFIT_MARGIN = 0.04
TRADING_BPS_THRESHOLD = 50
MAX_TRADING_BPS_THRESHOLD = 100
MARKET_SESSION_SECONDS = 900
TIMEZONE = "US/Eastern"
MAX_TRADES = 2
MAX_TRADES = 30
MAX_INVENTORY = 1
MIN_DELAY_BETWEEN_TRADES_SECONDS = 1
PLACE_OPPOSITE_ORDER = True # Hedge orders
-182
View File
@@ -1,182 +0,0 @@
use std::collections::HashSet;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::thread;
struct InMemSetDB {
data: Arc<Mutex<HashSet<String>>>,
}
impl InMemSetDB {
fn new() -> Self {
InMemSetDB {
data: Arc::new(Mutex::new(HashSet::new())),
}
}
fn add(&self, item: String) -> String {
let mut set = self.data.lock().unwrap();
set.insert(item);
r#"{"success":true}"#.to_string()
}
fn contains(&self, item: &str) -> String {
let set = self.data.lock().unwrap();
let exists = set.contains(item);
format!(r#"{{"exists":{}}}"#, exists)
}
fn clear(&self) -> String {
let mut set = self.data.lock().unwrap();
set.clear();
r#"{"success":true}"#.to_string()
}
fn size(&self) -> String {
let set = self.data.lock().unwrap();
format!(r#"{{"size":{}}}"#, set.len())
}
}
fn parse_request(request: &str) -> Option<(String, String, Option<String>)> {
let lines: Vec<&str> = request.split("\r\n").collect();
if lines.is_empty() {
return None;
}
let parts: Vec<&str> = lines[0].split_whitespace().collect();
if parts.len() < 2 {
return None;
}
let method = parts[0].to_string();
let path = parts[1].to_string();
let body = if method == "POST" {
if let Some(idx) = request.find("\r\n\r\n") {
Some(request[idx + 4..].to_string())
} else {
None
}
} else {
None
};
Some((method, path, body))
}
fn extract_json_field(json: &str, field: &str) -> Option<String> {
let search = format!(r#""{}":""#, field);
if let Some(start) = json.find(&search) {
let value_start = start + search.len();
if let Some(end) = json[value_start..].find('"') {
return Some(json[value_start..value_start + end].to_string());
}
}
None
}
fn handle_request(
set: &InMemSetDB,
method: &str,
path: &str,
body: Option<String>,
) -> (u16, String) {
match (method, path) {
("POST", "/add") => {
if let Some(body_str) = body {
if let Some(item) = extract_json_field(&body_str, "item") {
let response = set.add(item);
return (200, response);
}
return (400, r#"{"error":"Missing item"}"#.to_string());
}
(400, r#"{"error":"Missing body"}"#.to_string())
}
("POST", "/contains") => {
if let Some(body_str) = body {
if let Some(item) = extract_json_field(&body_str, "item") {
let response = set.contains(&item);
return (200, response);
}
return (400, r#"{"error":"Missing item"}"#.to_string());
}
(400, r#"{"error":"Missing body"}"#.to_string())
}
("POST", "/clear") => {
let response = set.clear();
(200, response)
}
("GET", "/size") => {
let response = set.size();
(200, response)
}
_ => (404, r#"{"error":"Not found"}"#.to_string()),
}
}
fn build_response(status: u16, body: String) -> String {
let status_text = match status {
200 => "OK",
400 => "Bad Request",
404 => "Not Found",
_ => "Unknown",
};
format!(
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status, status_text, body.len(), body
)
}
fn handle_client(mut stream: TcpStream, set: InMemSetDB) {
let mut buffer = [0; 4096];
match stream.read(&mut buffer) {
Ok(size) => {
if size == 0 {
return;
}
let request = String::from_utf8_lossy(&buffer[..size]);
if let Some((method, path, body)) = parse_request(&request) {
let (status, response_body) = handle_request(&set, &method, &path, body);
let response = build_response(status, response_body);
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
}
Err(e) => eprintln!("Error reading from stream: {}", e),
}
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").expect("Failed to bind");
let set = InMemSetDB::new();
println!("InMemSetDB started on http://127.0.0.1:8080");
println!("\nEndpoints:");
println!(r#" POST /add - {{"item": "value"}}"#);
println!(r#" POST /contains - {{"item": "value"}}"#);
println!(" POST /clear");
println!(" GET /size");
println!("\nWaiting for connections...\n");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let set_clone = InMemSetDB {
data: Arc::clone(&set.data),
};
thread::spawn(move || {
handle_client(stream, set_clone);
});
}
Err(e) => eprintln!("Connection failed: {}", e),
}
}
}
-48
View File
@@ -1,48 +0,0 @@
import socket
import time
import json
def fast_http_request(method, path, body=None):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 8080))
if body:
body_str = body
request = f"{method} {path} HTTP/1.1\r\nHost: localhost\r\nContent-Length: {len(body_str)}\r\n\r\n{body_str}"
else:
request = f"{method} {path} HTTP/1.1\r\nHost: localhost\r\n\r\n"
sock.send(request.encode())
response = sock.recv(4096).decode()
sock.close()
return response
def get_body_from_response(response):
return json.loads(response.split("\r\n\r\n", 1)[1])
def add_item(item):
body = f'{{"item":"{item}"}}'
response = fast_http_request("POST", "/add", body)
return response
def clear_items():
response = fast_http_request("POST", "/clear")
return response
def contains_item(item):
body = f'{{"item":"{item}"}}'
response = fast_http_request("POST", "/contains", body)
body = get_body_from_response(response)
return body["exists"]
def size():
response = fast_http_request("GET", "/size")
body = get_body_from_response(response)
return int(body["size"])
+47 -42
View File
@@ -1,48 +1,44 @@
import os
import gc
import asyncio
import requests
import time
from utils.logger import setup_logging
from utils.tokens import fetch_tokens
from utils.orderbook import OrderBook, SIGNALES
from utils.clob_client import init_global_client, is_client_ready
from utils.market_time import is_in_trading_window
from utils.market_time import is_in_trading_window, get_period_elapsed_seconds
from utils.trade_counter import reset_trades, get_trades_count, increment_trades
from utils.clob_orders import (
place_anchor_and_hedge,
cache_token_trading_infos,
)
from utils.cpu_affinity import set_cpu_affinity
from config import MAX_TRADES
from config import (
MAX_TRADES,
MAX_TRADING_BPS_THRESHOLD,
MIN_DELAY_BETWEEN_TRADES_SECONDS,
MAX_INVENTORY,
PROFIT_MARGIN,
)
gc.disable()
session = requests.Session()
requests.get = session.get
requests.post = session.post
requests.put = session.put
requests.patch = session.patch
requests.delete = session.delete
requests.head = session.head
requests.options = session.options
async def main():
def main():
logger = setup_logging()
set_cpu_affinity()
logger.info("Polymarket HFT Market Maker started")
init_global_client()
await asyncio.sleep(2)
time.sleep(2)
if not is_client_ready():
logger.error("ClobClient is not ready. Exiting.")
return
up_token, down_token, market_slug = await fetch_tokens()
up_token, down_token, market_slug = fetch_tokens()
book = OrderBook(up_token, down_token, market_slug)
await asyncio.create_task(cache_token_trading_infos(book))
book.start()
await asyncio.sleep(5) # Allow some time for initial order book data
time.sleep(5) # Allow some time for initial order book data
market_data = book.get_current_market_data()
@@ -52,7 +48,7 @@ async def main():
down_bid_price = 1 - up_ask_price
print(
f"Initial Prices - UP: {up_bid_price:.2f}/{up_ask_price:.2f} | DOWN: {down_bid_price:.2f}/{down_ask_price:.2f}",
f"Initial Prices - UP: {up_bid_price:.2f}/{up_ask_price:.2f} | DOWN: {down_bid_price:.2f}/{down_ask_price:.2f} | Inventory: {book.inventory} / {MAX_INVENTORY}",
flush=True,
)
@@ -62,11 +58,11 @@ async def main():
book.stop()
logger.info("Trading session ended. Starting new session.")
gc.collect()
await asyncio.sleep(10)
time.sleep(10)
reset_trades()
up_token, down_token, market_slug = await fetch_tokens()
up_token, down_token, market_slug = fetch_tokens()
book = OrderBook(up_token, down_token, market_slug)
asyncio.create_task(cache_token_trading_infos(book))
cache_token_trading_infos(book)
book.start()
market_data = book.get_current_market_data()
@@ -76,54 +72,63 @@ async def main():
up_bid_price = market_data["best_bid_price"]
up_ask_price = market_data["best_ask_price"]
if not ((0.2 < up_ask_price < 0.35) or (0.65 < up_bid_price < 0.8)):
if not ((0.2 < up_ask_price < 0.35) or (0.65 < up_bid_price < 0.8)) or (
abs(market_data["micro_vs_mid_bps"]) > MAX_TRADING_BPS_THRESHOLD
):
continue
down_ask_price = 1 - up_bid_price
down_bid_price = 1 - up_ask_price
up_trend = up_bid_price > down_bid_price
if get_trades_count() < MAX_TRADES:
if (
(get_trades_count() < MAX_TRADES)
and (get_period_elapsed_seconds() < 500)
and (book.inventory < MAX_INVENTORY)
):
trading_side = book.last_signal
if (trading_side == SIGNALES.UP) and not up_trend:
await place_anchor_and_hedge(
if trading_side == SIGNALES.UP:
order_ids = place_anchor_and_hedge(
up_token,
down_token,
"UP",
up_bid_price,
round(up_bid_price, 2),
size=5,
signed_orders_cache=book.signed_orders_cache,
)
current_trades = increment_trades()
logger.info(
f"Placed UP anchor and hedge orders. Total trades: {current_trades}"
book.update_signed_orders_cache(
[round(up_bid_price, 2), round(1 - up_bid_price - PROFIT_MARGIN, 2)]
)
logger.info(
f"Placed UP anchor and hedge orders. Total trades: {current_trades}, Order IDs: {order_ids}"
)
time.sleep(MIN_DELAY_BETWEEN_TRADES_SECONDS)
elif (trading_side == SIGNALES.DOWN) and up_trend:
await place_anchor_and_hedge(
elif trading_side == SIGNALES.DOWN:
order_ids = place_anchor_and_hedge(
up_token,
down_token,
"DOWN",
down_bid_price,
round(down_bid_price, 2),
size=5,
signed_orders_cache=book.signed_orders_cache,
)
current_trades = increment_trades()
logger.info(
f"Placed DOWN anchor and hedge orders. Total trades: {current_trades}"
book.update_signed_orders_cache(
[round(down_bid_price, 2), round(1 - down_bid_price - PROFIT_MARGIN, 2)]
)
logger.info(
f"Placed DOWN anchor and hedge orders. Total trades: {current_trades}, Order IDs: {order_ids}"
)
time.sleep(MIN_DELAY_BETWEEN_TRADES_SECONDS)
await asyncio.sleep(0.01)
time.sleep(0.01)
if __name__ == "__main__":
try:
if os.name == "nt":
asyncio.run(main())
else:
import uvloop
uvloop.run(main())
main()
except KeyboardInterrupt:
print("\nMarket maker stopped by user")
except Exception as e:
+215
View File
@@ -0,0 +1,215 @@
import os
import time
import requests
import asyncio
from web3 import Web3
from eth_account import Account
from dotenv import load_dotenv
from web3.middleware import ExtraDataToPOAMiddleware
from abi.ctfAbi import ctf_abi
from abi.safeAbi import safe_abi
# Constants
CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
NEG_RISK_ADAPTER_ADDRESS = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
USDC_ADDRESS = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"
USDCE_DIGITS = 6
load_dotenv()
def merge_tokens(condition_id, amount=None, neg_risk=False):
"""
Merge conditional tokens back to USDC.
Args:
condition_id: The condition ID (bytes32 hex string)
amount: Amount to merge in USDC (e.g., "1.5"). If None, merges all available tokens
neg_risk: Whether to use NEG_RISK_ADAPTER (default: False)
Returns:
bool: True if merge successful, False otherwise
"""
try:
# Connect to provider
rpc_url = os.getenv("RPC_URL")
w3 = Web3(Web3.HTTPProvider(rpc_url))
# Inject POA middleware for Polygon
from web3.middleware import ExtraDataToPOAMiddleware
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
# Load wallet and safe
private_key = os.getenv("PRIVATE_KEY")
account = Account.from_key(private_key)
safe_address = Web3.to_checksum_address(os.getenv("POLYMARKET_PROXY_ADDRESS"))
safe = w3.eth.contract(address=safe_address, abi=safe_abi)
# Determine merge amount
if amount is None:
# Get minimum balance of both positions
ctf_contract = w3.eth.contract(
address=Web3.to_checksum_address(CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS),
abi=ctf_abi,
)
parent_collection_id = bytes(32)
collection_id_0 = ctf_contract.functions.getCollectionId(
parent_collection_id, bytes.fromhex(condition_id[2:]), 1
).call()
collection_id_1 = ctf_contract.functions.getCollectionId(
parent_collection_id, bytes.fromhex(condition_id[2:]), 2
).call()
position_id_0 = ctf_contract.functions.getPositionId(
Web3.to_checksum_address(USDC_ADDRESS), collection_id_0
).call()
position_id_1 = ctf_contract.functions.getPositionId(
Web3.to_checksum_address(USDC_ADDRESS), collection_id_1
).call()
balance_0 = ctf_contract.functions.balanceOf(
safe_address, position_id_0
).call()
balance_1 = ctf_contract.functions.balanceOf(
safe_address, position_id_1
).call()
amount_wei = min(balance_0, balance_1)
if amount_wei == 0:
print("Merge failed: No tokens to merge")
return False
else:
amount_wei = int(float(amount) * (10**USDCE_DIGITS))
# Encode merge transaction
ctf_contract = w3.eth.contract(abi=ctf_abi)
parent_collection_id = (
"0x0000000000000000000000000000000000000000000000000000000000000000"
)
partition = [1, 2]
data = ctf_contract.functions.mergePositions(
Web3.to_checksum_address(USDC_ADDRESS),
bytes.fromhex(parent_collection_id[2:]),
bytes.fromhex(condition_id[2:]),
partition,
amount_wei,
)._encode_transaction_data()
# Sign and execute Safe transaction
nonce = safe.functions.nonce().call()
to = (
NEG_RISK_ADAPTER_ADDRESS
if neg_risk
else CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS
)
tx_hash = safe.functions.getTransactionHash(
Web3.to_checksum_address(to),
0,
bytes.fromhex(data[2:]),
0, # operation: Call
0,
0,
0, # safeTxGas, baseGas, gasPrice
"0x0000000000000000000000000000000000000000", # gasToken
"0x0000000000000000000000000000000000000000", # refundReceiver
nonce,
).call()
# Sign the hash
hash_bytes = Web3.to_bytes(
hexstr=tx_hash.hex() if hasattr(tx_hash, "hex") else tx_hash
)
signature_obj = account.unsafe_sign_hash(hash_bytes)
r = signature_obj.r.to_bytes(32, byteorder="big")
s = signature_obj.s.to_bytes(32, byteorder="big")
v = signature_obj.v.to_bytes(1, byteorder="big")
signature = r + s + v
# Build and send transaction
tx = safe.functions.execTransaction(
Web3.to_checksum_address(to),
0,
bytes.fromhex(data[2:]),
0,
0,
0,
0,
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000",
signature,
).build_transaction(
{
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 500000,
"gasPrice": w3.eth.gas_price,
}
)
signed_tx = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
# Wait for receipt
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
# Check if transaction succeeded
if receipt["status"] == 1:
print(f"Merge successful! Amount: {amount_wei / 10**USDCE_DIGITS} USDC")
return True
else:
print("Merge failed: Transaction reverted")
return False
except Exception as e:
print(f"Merge failed: {str(e)}")
return False
private_key = os.getenv("PRIVATE_KEY")
account = Account.from_key(private_key)
safe_address = Web3.to_checksum_address(os.getenv("POLYMARKET_PROXY_ADDRESS"))
w3 = Web3(Web3.HTTPProvider(os.getenv("RPC_URL")))
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
matic_balance = w3.eth.get_balance(account.address)
proxy_balance = w3.eth.get_balance(safe_address)
print(f"--- Wallet Check ---")
print(f"Signer Address (from Private Key): {account.address}")
print(f"Signer POL Balance: {w3.from_wei(matic_balance, 'ether')} POL")
print(f"Proxy Address (Safe): {safe_address}")
print(f"Proxy POL Balance: {w3.from_wei(proxy_balance, 'ether')} POL")
print(f"--------------------")
if matic_balance == 0:
raise Exception(f"STOP: Your Signer address {account.address} has NO gas!")
async def do_it():
url = f"https://data-api.polymarket.com/positions?sizeThreshold=1&limit=100&sortBy=TOKENS&sortDirection=DESC&user={os.getenv('POLYMARKET_PROXY_ADDRESS')}&mergeable=true"
response = requests.get(url).json()
if not response:
print("No mergeable positions found.")
return
condition_ids = set([token["conditionId"] for token in response])
condition_ids
for condition_id in condition_ids:
print("Merging tokens for condition ID:", condition_id)
merge_tokens(condition_id)
async def main():
while True:
asyncio.create_task(do_it())
await asyncio.sleep(10)
if __name__ == "__main__":
asyncio.run(main())
-93
View File
@@ -1,93 +0,0 @@
import asyncio
import json
import logging
import os
import sys
import websockets
from utils.clob_client import get_client
from config import POLYMARKET_WS_USER_URL
from in_memory_db.utils import add_item as in_memory_db_add_item
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("OrderListener")
client = get_client()
creds = client.create_or_derive_api_creds()
async def handle_message(message):
try:
data = json.loads(message)
logger.debug(f"Received Message: {data}")
data_type = data.get("type")
if data_type in ["PLACEMENT", "CANCELLATION"]:
return
orders_ids = []
orders_ids.append(data.get("id"))
if data_type == "TRADE":
for orders in data.get("maker_orders", []):
orders_ids.append(orders.get("order_id"))
for order_id in orders_ids:
in_memory_db_add_item(order_id)
logger.info(f"Added Order ID: {order_id} into in-memory DB")
except json.JSONDecodeError:
logger.error("Failed to decode message as JSON")
return
async def hedger_main():
logger.info("🔌 Connecting to WebSocket...")
while True:
try:
async with websockets.connect(
POLYMARKET_WS_USER_URL,
ping_interval=10,
ping_timeout=10,
close_timeout=5,
) as ws:
auth_payload = {
"type": "user",
"auth": {
"apiKey": creds.api_key,
"secret": creds.api_secret,
"passphrase": creds.api_passphrase,
},
}
await ws.send(json.dumps(auth_payload))
logger.info("🌐 WS Authenticated. Listening Orders...")
async for message in ws:
await handle_message(message)
except (websockets.ConnectionClosed, OSError, ConnectionRefusedError) as e:
logger.warning(f"🔌 Disconnected: {e}. Reconnecting...")
continue
except Exception as e:
logger.error(f"Unexpected Error: {e}", exc_info=True)
continue
if __name__ == "__main__":
try:
if os.name != "nt":
import uvloop
uvloop.run(hedger_main())
else:
asyncio.run(hedger_main())
except KeyboardInterrupt:
logger.info("Order Listener Stopped by User")
except Exception as e:
logger.critical(f"Fatal Error: {e}", exc_info=True)
exit(1)
+2 -2
View File
@@ -1,3 +1,3 @@
nohup in_memory_db/in_mem_db &
nohup python order_listener.py &
source ~/miniconda3/bin/activate
nohup python merger.py &
sudo env "PATH=$PATH" python main.py
-5
View File
@@ -5,8 +5,3 @@ rm ~/miniconda3/miniconda.sh
source ~/miniconda3/bin/activate
conda init --all
pip install -r requirements.txt
sudo apt update -y
sudo apt install build-essential curl -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
rustc in_memory_db/in_mem_db.rs -o in_memory_db/in_mem_db
+47 -27
View File
@@ -1,18 +1,18 @@
import asyncio
import logging
import time
from concurrent.futures import ThreadPoolExecutor
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY
from config import PROFIT_MARGIN, PLACE_OPPOSITE_ORDER
from utils.clob_client import get_client
from utils.trade_counter import decrement_trades
from in_memory_db.utils import contains_item as in_memory_db_contains_item
logger = logging.getLogger(__name__)
async def cache_token_trading_infos(
def cache_token_trading_infos(
order_book,
) -> None:
client = get_client()
@@ -26,8 +26,8 @@ async def cache_token_trading_infos(
client.get_fee_rate_bps(down_token_id)
async def place_anchor_and_hedge(
up_token_id, down_token_id, anchor_side, price, size=5
def place_anchor_and_hedge(
up_token_id, down_token_id, anchor_side, price, size=5, signed_orders_cache=None
):
if anchor_side == "UP":
anchor_token_id = up_token_id
@@ -36,35 +36,52 @@ async def place_anchor_and_hedge(
anchor_token_id = down_token_id
hedge_token_id = up_token_id
asyncio.create_task(place_limit_order(anchor_token_id, price, size))
asyncio.create_task(
place_limit_order(hedge_token_id, 1 - price - PROFIT_MARGIN, size)
)
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(
place_limit_order_sync,
anchor_token_id,
price,
size,
signed_orders_cache,
)
future2 = executor.submit(
place_limit_order_sync,
hedge_token_id,
round(1 - price - PROFIT_MARGIN, 2),
size,
signed_orders_cache,
)
# Wait for both to complete
order_ids = [future1.result(), future2.result()]
logger.info(
f"Placed anchor and hedge orders: Anchor Token ID={anchor_token_id}, Hedge Token ID={hedge_token_id}"
f"Placed anchor and hedge orders: Anchor Token ID={anchor_token_id}, Hedge Token ID={hedge_token_id}, Order IDs={order_ids}"
)
return order_ids
async def place_limit_order(token_id: str, price: float, size: int, expire=False):
def place_limit_order_sync(
token_id: str, price: float, size: int = 5, signed_orders_cache=None
) -> str:
"""Synchronous version of place_limit_order for use with ThreadPoolExecutor"""
client = get_client()
expiration = 0
if expire:
one_minute = 60
desired_seconds = 5
expiration = int(time.time()) + one_minute + desired_seconds
try:
order_args = OrderArgs(
token_id=token_id,
price=price,
size=size,
side=BUY,
expiration=expiration,
)
signed_order = client.create_order(order_args)
response = client.post_order(
signed_order, OrderType.GTD if expire else OrderType.GTC
)
try:
if signed_orders_cache and (token_id, price) in signed_orders_cache:
signed_order = signed_orders_cache[(token_id, price)]
logger.info(
f"Using cached signed order for Token ID={token_id}, Price={price}"
)
else:
order_args = OrderArgs(
token_id=token_id,
price=price,
size=size,
side=BUY,
)
signed_order = client.create_order(order_args)
response = client.post_order(signed_order)
logger.info(
f"Placed limit order: Token ID={token_id}, Price={price}, Size={size}, ID={response['orderID']}"
)
@@ -72,3 +89,6 @@ async def place_limit_order(token_id: str, price: float, size: int, expire=False
except Exception as e:
logger.error(f"Error placing order for token {token_id}: {e}")
return None
+14
View File
@@ -0,0 +1,14 @@
import requests
import os
def get_inventory(slug=None):
url = f"https://data-api.polymarket.com/positions?sizeThreshold=1&user={os.getenv('POLYMARKET_PROXY_ADDRESS')}&mergeable=false"
response = requests.get(url).json()
size = 0
for pos in response:
if pos and pos.get("slug") == slug:
size += pos.get("size")
return round(size / 5)
+73 -8
View File
@@ -1,5 +1,4 @@
import os
import asyncio
import bisect
import time
import json
@@ -8,7 +7,10 @@ import threading
import websocket
from enum import Enum
from config import POLYMARKET_WS_MARKET_URL, TRADING_BPS_THRESHOLD
from py_clob_client import OrderArgs
from py_clob_client.order_builder.constants import BUY
from utils.clob_client import get_client
from utils.inventory import get_inventory
logger = logging.getLogger(__name__)
@@ -34,6 +36,8 @@ class OrderBook:
"order_book": {"bids": [], "asks": []},
}
self.signed_orders_cache = {}
self.ws = None
self.running = False
self.thread = None
@@ -43,6 +47,10 @@ class OrderBook:
self.monitoring_running = False
self.last_signal = SIGNALES.NEUTRAL
self.inventory = 0
self.inventory_thread = None
self.inventory_running = False
self.create_signed_orders_cache()
def _on_message(self, ws, message):
@@ -100,25 +108,46 @@ class OrderBook:
self.running = True
self.monitoring_running = True
self.inventory_running = True
self.thread = threading.Thread(target=self._connect, daemon=True)
self.thread.start()
self.monitoring_thread = threading.Thread(
target=lambda: asyncio.run(self._continuous_trading_monitor()), daemon=True
target=self._continuous_trading_monitor, daemon=True
)
self.monitoring_thread.start()
logger.info("WebSocket price stream and trading monitor started")
self.inventory_thread = threading.Thread(
target=self._inventory_updater, daemon=True
)
self.inventory_thread.start()
logger.info(
"WebSocket price stream, trading monitor, and inventory updater started"
)
def stop(self):
self.running = False
self.monitoring_running = False
self.inventory_running = False
if self.ws:
self.ws.close()
logger.info("🛑 WebSocket price stream and trading monitor stopped")
logger.info(
"🛑 WebSocket price stream, trading monitor, and inventory updater stopped"
)
def _inventory_updater(self):
logger.info("Started inventory updater thread")
while self.inventory_running:
try:
self.inventory = get_inventory(self.slug)
except Exception as e:
logger.error(f"Error updating inventory: {e}")
time.sleep(1)
logger.info("Stopped inventory updater thread")
def is_connected(self):
with self.lock:
@@ -177,14 +206,14 @@ class OrderBook:
"asks": asks,
}
async def _continuous_trading_monitor(self):
def _continuous_trading_monitor(self):
logger.info("Started continuous trading monitor")
while self.monitoring_running:
try:
market_data = self.get_current_market_data()
if not market_data:
await asyncio.sleep(0.1)
time.sleep(0.1)
continue
micro_vs_mid_bps = market_data["micro_vs_mid_bps"]
@@ -200,14 +229,50 @@ class OrderBook:
if current_signal and current_signal != self.last_signal:
self.last_signal = current_signal
await asyncio.sleep(0.005)
time.sleep(0.005)
except Exception as e:
logger.error(f"Error in continuous trading monitor: {e}")
await asyncio.sleep(1)
time.sleep(1)
logger.info("Stopped continuous trading monitor")
def create_signed_orders_cache(self):
start = time.time()
prices = [0.01]
while prices[-1] < 0.99:
prices.append(round(prices[-1] + 0.01, 2))
client = get_client()
for price in prices:
for token_id in [self.up_token_id, self.down_token_id]:
order_args = OrderArgs(
token_id=token_id,
price=price,
size=5,
side=BUY,
)
signed_order = client.create_order(order_args)
self.signed_orders_cache[(token_id, price)] = signed_order
end = time.time()
logger.info(
f"Pre-created signed orders cache for tokens in {round((end - start) * 1000)} milliseconds"
)
def update_signed_orders_cache(self, prices):
client = get_client()
for price in prices:
for token_id in [self.up_token_id, self.down_token_id]:
order_args = OrderArgs(
token_id=token_id,
price=price,
size=5,
side=BUY,
)
signed_order = client.create_order(order_args)
self.signed_orders_cache[(token_id, price)] = signed_order
logger.info(f"Updated signed orders cache for new prices: {prices}")
def clear_screen(self):
os.system("cls" if os.name == "nt" else "clear")
+9 -12
View File
@@ -2,7 +2,7 @@ import json
import logging
from multiprocessing.util import get_logger
from typing import Optional, Tuple
import aiohttp
import requests
from .slug import get_market_slug
from config import GAMMA_API_URL, REQUEST_TIMEOUT
@@ -10,7 +10,7 @@ from config import GAMMA_API_URL, REQUEST_TIMEOUT
logger = logging.getLogger(__name__)
async def fetch_tokens(
def fetch_tokens(
coin: str = "btc",
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
@@ -21,17 +21,14 @@ async def fetch_tokens(
slug = get_market_slug(coin)
url = f"{GAMMA_API_URL}/events/slug/{slug}"
async with aiohttp.ClientSession() as session:
async with session.get(
url, timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
) as response:
if response.status == 200:
data = await response.json()
return _extract_tokens(data, slug)
else:
logger.warning(f"API request failed with status {response.status}")
response = requests.get(url, timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
data = response.json()
return _extract_tokens(data, slug)
else:
logger.warning(f"API request failed with status {response.status_code}")
except aiohttp.ClientError as e:
except requests.exceptions.RequestException as e:
logger.error(f"Network error fetching tokens: {e}")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON response: {e}")