diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b81679 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +logs/ +__pycache__/ diff --git a/config.py b/config.py new file mode 100644 index 0000000..b0aff10 --- /dev/null +++ b/config.py @@ -0,0 +1,3 @@ +LOG_FOLDER = "logs/" +GAMMA_API_URL = "https://gamma-api.polymarket.com" +REQUEST_TIMEOUT = 5 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..73b864a --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +import asyncio +from utils.logger import setup_logging +from utils.tokens import fetch_tokens + + +async def main(): + logger = setup_logging() + logger.info("Polymarket HFT Market Maker started") + + up_token, down_token, market_slug = await fetch_tokens() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nMarket maker stopped by user") + except Exception as e: + print(f"Fatal error: {e}") + exit(1) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..ad00db8 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,27 @@ +import logging +from pathlib import Path +from datetime import datetime +from config import LOG_FOLDER + + +def setup_logging(): + logs_dir = Path(LOG_FOLDER) + logs_dir.mkdir(exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + log_filename = logs_dir / f"polymarket_hft_{timestamp}.log" + + log_format = "%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s" + date_format = "%Y-%m-%d %H:%M:%S" + + logging.basicConfig( + level=logging.INFO, + format=log_format, + datefmt=date_format, + handlers=[ + logging.FileHandler(log_filename, encoding="utf-8"), + logging.StreamHandler(), + ], + ) + + return logging.getLogger(__name__) diff --git a/utils/slug.py b/utils/slug.py new file mode 100644 index 0000000..eabadf0 --- /dev/null +++ b/utils/slug.py @@ -0,0 +1,18 @@ +import pytz +from datetime import datetime + + +MARKET_INTERVAL_SECONDS = 900 +TIMEZONE = "US/Eastern" + + +def get_market_slug(coin: str = "btc") -> str: + + if not coin or not isinstance(coin, str): + raise ValueError("Coin must be a non-empty string") + + et_tz = pytz.timezone(TIMEZONE) + now = datetime.now(et_tz) + ts = int(now.timestamp()) + start = (ts // MARKET_INTERVAL_SECONDS) * MARKET_INTERVAL_SECONDS + return f"{coin.lower()}-updown-15m-{start}" diff --git a/utils/tokens.py b/utils/tokens.py new file mode 100644 index 0000000..1043abd --- /dev/null +++ b/utils/tokens.py @@ -0,0 +1,100 @@ +import json +import logging +from multiprocessing.util import get_logger +from typing import Optional, Tuple +import aiohttp +from .slug import get_market_slug +from config import GAMMA_API_URL, REQUEST_TIMEOUT + + +logger = logging.getLogger(__name__) + + +async def fetch_tokens( + coin: str = "btc", +) -> Tuple[Optional[str], Optional[str], Optional[str]]: + + if not coin or not isinstance(coin, str): + raise ValueError("Coin must be a non-empty string") + + try: + 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}") + + except aiohttp.ClientError as e: + logger.error(f"Network error fetching tokens: {e}") + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON response: {e}") + except ValueError as e: + logger.error(f"Invalid input: {e}") + raise + except Exception as e: + logger.error(f"Unexpected error fetching tokens: {e}") + + return None, None, None + + +def _extract_tokens( + data: dict, slug: str +) -> Tuple[Optional[str], Optional[str], Optional[str]]: + + try: + if not isinstance(data, dict) or "markets" not in data: + logger.error("Invalid response structure: missing 'markets' field") + return None, None, None + + markets = data["markets"] + if not isinstance(markets, list) or len(markets) == 0: + logger.error("Invalid response structure: 'markets' is empty or not a list") + return None, None, None + + market = markets[0] + if not isinstance(market, dict) or "clobTokenIds" not in market: + logger.error("Invalid market structure: missing 'clobTokenIds'") + return None, None, None + + clob_token_ids = market["clobTokenIds"] + if not isinstance(clob_token_ids, str): + logger.error("Invalid token IDs format: not a string") + return None, None, None + + try: + token_list = json.loads(clob_token_ids) + if not isinstance(token_list, list) or len(token_list) < 2: + logger.error("Invalid token list format or insufficient tokens") + return None, None, None + + up_token = token_list[0] + down_token = token_list[1] + + if not ( + isinstance(up_token, str) + and isinstance(down_token, str) + and up_token.isdigit() + and down_token.isdigit() + ): + logger.error("Invalid token ID format") + return None, None, None + + logger.info( + f"Successfully extracted tokens for market {slug}: \n\tUP : {up_token} \n\tDOWN : {down_token}" + ) + return up_token, down_token, slug + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse token IDs JSON: {e}") + return None, None, None + + except Exception as e: + logger.error(f"Unexpected error extracting tokens: {e}") + return None, None, None