Compare commits
29 Commits
old-approch
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef4cb47d06 | |||
| 5657b087c5 | |||
| d7e2f8d04f | |||
| 02bb3eefee | |||
| e88752cb23 | |||
| fa31c52cc3 | |||
| c1150f927a | |||
| c97f96765c | |||
| 3eeea819a9 | |||
| 768ec1358c | |||
| bbfb33829e | |||
| 7508654d00 | |||
| 5a579d49c0 | |||
| da9244eabd | |||
| 7abcd3642c | |||
| 25d7e2a2d4 | |||
| fd0e2b31b1 | |||
| ca8e167275 | |||
| 351a76a308 | |||
| b4c3eb90c9 | |||
| 430f2e9ad0 | |||
| 32518bfceb | |||
| e0cd82553a | |||
| 1de0ebd22e | |||
| bf1c3515c0 | |||
| a67afb031c | |||
| 145fc5e6f4 | |||
| a31995f362 | |||
| 98fe566336 |
+721
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"])
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user