Compare commits

...
Author SHA1 Message Date
pselamyandClaude Opus 4.6 de5e2592e8 docs: fix README to match actual project structure and tooling
- Replace pip commands with uv equivalents
- Fix run command from 'python -m src.main' to 'python -m polymarket_insider_tracker'
- Update project structure tree to reflect actual src/polymarket_insider_tracker/ layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 02:02:52 +00:00
pselamyandClaude Opus 4.6 c6f41ea29f fix: remove deprecated version key from docker-compose.yml
The top-level 'version' key is obsolete in modern Docker Compose
and produces a warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 02:02:50 +00:00
pselamyandClaude Opus 4.6 9e46f17538 fix: resolve ruff lint errors in alembic migration
- Replace typing.Union with X | Y syntax (UP007)
- Import Sequence from collections.abc instead of typing (UP035)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 02:02:48 +00:00
pselamyandClaude Opus 4.6 6839e7dc47 fix: replace deprecated datetime.utcnow() and websockets.legacy APIs
- Replace datetime.utcnow() with datetime.now(UTC) in Orderbook model
- Use websockets.asyncio.client.connect instead of legacy websockets.connect
- Import ConnectionClosed from websockets.exceptions directly
- Update test mocks to patch the new import paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 02:02:44 +00:00
7c494c38a6 fix: replace (str, Enum) with StrEnum to resolve ruff UP042 (#87)
StrEnum (Python 3.11+) is the modern replacement for the (str, Enum)
pattern. Updates SyncState and PipelineState enums.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:17:51 -04:00
17504a5eaa fix: replace (str, Enum) with StrEnum to resolve ruff UP042 (#86)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:16:23 -04:00
cadc593d62 fix: replace (str, Enum) with StrEnum to resolve ruff UP042 (#85)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:14:36 -04:00
0320a96f8a fix: replace (str, Enum) with StrEnum (ruff UP042) (#84)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:12:59 -04:00
753d6cf1de fix: replace (str, Enum) with StrEnum (ruff UP042) (#83)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:11:19 -04:00
f3801987fe fix: resolve .env.example variable interpolation and alembic env var mismatch (#62) (#82)
.env files don't support shell variable expansion, so DATABASE_URL and
REDIS_URL contained literal ${...} strings. Also alembic/env.py read
SQLALCHEMY_DATABASE_URL instead of DATABASE_URL.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:05:00 -04:00
5cbcaccd82 fix: resolve alembic migration failures from .env and env var mismatch (#81)
- Replace shell variable interpolation in .env.example with literal
  values since .env files don't support variable expansion
- Change alembic/env.py to read DATABASE_URL (matching app convention)
  instead of SQLALCHEMY_DATABASE_URL

Closes #62

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:03:58 -04:00
288fbc75cf fix: resolve broken alembic migrations (#62) (#80)
- Replace shell variable interpolation in .env.example with literal values
- Change alembic/env.py to read DATABASE_URL instead of SQLALCHEMY_DATABASE_URL

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:02:53 -04:00
37ea0c6540 fix: resolve broken alembic migrations (#62) (#79)
- Replace shell variable interpolation in .env.example with literal values
- Change alembic/env.py to read DATABASE_URL instead of SQLALCHEMY_DATABASE_URL

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:02:02 -04:00
654f4466f0 fix: resolve broken alembic migrations (#62) (#78)
- Replace shell variable interpolation in .env.example with literal
  values since .env files don't expand variables
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's env var name

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:00:56 -04:00
8628e3aadb fix: use literal values in .env.example and correct env var name in alembic (#77)
.env files don't support shell variable interpolation, so DATABASE_URL
and REDIS_URL had literal ${VAR} strings instead of actual values.
Also alembic/env.py read SQLALCHEMY_DATABASE_URL instead of DATABASE_URL.

Closes #62

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:59:46 -04:00
d808819990 fix: resolve alembic migration setup bugs (#62) (#76)
- Replace shell variable interpolation in .env.example with literal
  default values (.env files don't expand variables)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's convention

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:58:34 -04:00
20b8ad992c fix: resolve alembic migration setup bugs (#62) (#75)
- Replace shell variable interpolation in .env.example with literal
  default values since .env files don't expand variables
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's env var name

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:57:33 -04:00
bd320f1a8e fix: resolve alembic migration setup bugs (#62) (#74)
- Replace shell variable interpolation in .env.example with literal
  values (.env files don't expand variables)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match app configuration

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:56:30 -04:00
c65ed2225f fix: resolve alembic migration setup bugs (#62) (#73)
- Replace shell variable interpolation in .env.example with literal
  values (.env files don't expand variables)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match app convention

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:55:24 -04:00
06ff48117c fix: use literal values in .env.example and correct env var name in alembic (#72)
.env files don't support shell variable interpolation, so DATABASE_URL
and REDIS_URL had literal ${VAR} strings instead of actual values.
Also alembic/env.py was reading SQLALCHEMY_DATABASE_URL instead of
DATABASE_URL, which is what the app and .env use.

Closes #62

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:54:15 -04:00
34393e7ee3 fix: resolve alembic migration failures from env config bugs (#62) (#71)
- Replace shell variable interpolation in .env.example with literal
  values (.env files don't expand ${VAR} syntax)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's env var name

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:53:02 -04:00
cf66aac9e3 fix: resolve alembic migration failures from env config bugs (#62) (#70)
- Replace shell variable interpolation in .env.example with literal
  values (.env files don't expand ${VAR} syntax)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's env var name

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:52:16 -04:00
13d5945b01 fix: resolve alembic migration failures from env config bugs (#62) (#69)
- Replace shell variable interpolation in .env.example with literal values
- Fix alembic/env.py to read DATABASE_URL instead of SQLALCHEMY_DATABASE_URL

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:51:13 -04:00
7352143004 fix: resolve two bugs preventing alembic migrations (#62) (#68)
1. Replace shell variable interpolation in .env.example with literal
   values, since .env files don't expand variables.
2. Change alembic/env.py to read DATABASE_URL instead of
   SQLALCHEMY_DATABASE_URL to match app convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:49:57 -04:00
e05ac2ca64 fix: resolve two bugs preventing alembic migrations (#62) (#67)
1. Replace shell variable interpolation in .env.example with literal
   values, since .env files don't expand variables.
2. Change alembic/env.py to read DATABASE_URL instead of
   SQLALCHEMY_DATABASE_URL to match app convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:49:18 -04:00
5d533c8c79 fix: resolve two bugs preventing alembic migrations (#62) (#66)
1. Replace shell variable interpolation in .env.example with literal
   values, since .env files don't expand variables.
2. Change alembic/env.py to read DATABASE_URL instead of
   SQLALCHEMY_DATABASE_URL to match app convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:48:25 -04:00
e67f2d6d8f fix: resolve alembic migration setup bugs (#62) (#65)
- Replace shell variable interpolation in .env.example with literal values
- Change alembic/env.py to read DATABASE_URL instead of SQLALCHEMY_DATABASE_URL

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:47:23 -04:00
387a10132b fix: resolve alembic migration setup bugs (#62) (#64)
- Replace shell variable interpolation in .env.example with literal
  default values (.env files don't expand variables)
- Change alembic/env.py to read DATABASE_URL instead of
  SQLALCHEMY_DATABASE_URL to match the app's env var name

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:46:25 -04:00
b044bf25be fix: resolve broken alembic migrations (#62) (#63)
- Replace shell variable interpolation in .env.example with literal
  values, since .env files don't expand shell variables
- Fix alembic/env.py to read DATABASE_URL instead of SQLALCHEMY_DATABASE_URL
  to match the app's environment variable convention

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:45:21 -04:00
10 changed files with 42 additions and 45 deletions
+2 -2
View File
@@ -6,12 +6,12 @@ POSTGRES_USER=tracker
POSTGRES_PASSWORD=dev_password POSTGRES_PASSWORD=dev_password
# Constructed database URL (for application use) # Constructed database URL (for application use)
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} DATABASE_URL=postgresql://tracker:dev_password@localhost:5432/polymarket_tracker
# Redis Configuration # Redis Configuration
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} REDIS_URL=redis://localhost:6379
# Optional: Development tool ports # Optional: Development tool ports
ADMINER_PORT=8080 ADMINER_PORT=8080
+17 -25
View File
@@ -126,13 +126,13 @@ docker compose up -d
docker compose ps docker compose ps
# Install Python dependencies # Install Python dependencies
pip install -e . uv sync --all-extras
# Run database migrations # Run database migrations
alembic upgrade head uv run alembic upgrade head
# Run the tracker # Run the tracker
python -m src.main uv run python -m polymarket_insider_tracker
``` ```
### Docker Services ### Docker Services
@@ -188,24 +188,16 @@ LIQUIDITY_IMPACT_THRESHOLD=0.02
``` ```
polymarket-insider-tracker/ polymarket-insider-tracker/
├── src/ ├── src/
── ingestor/ # Real-time market data ingestion ── polymarket_insider_tracker/
├── clob_client.py # Polymarket CLOB API wrapper ├── __main__.py # CLI entry point
── websocket.py # WebSocket event handler ── pipeline.py # Core detection pipeline
├── profiler/ # Wallet analysis ├── ingestor/ # Real-time market data ingestion
│ │ ├── analyzer.py # Core wallet profiling logic │ ├── clob_client.py # Polymarket CLOB API wrapper
│ │ ── chain.py # Polygon blockchain client ── websocket.py # WebSocket event handler
── funding.py # Funding chain tracer ── profiler/ # Wallet analysis
│ ├── detector/ # Anomaly detection engines ├── detector/ # Anomaly detection engines
├── fresh_wallet.py ├── alerter/ # Notification dispatch
── size_anomaly.py ── storage/ # Persistence layer
│ │ ├── sniper.py # DBSCAN clustering
│ │ └── scorer.py # Composite risk scoring
│ ├── alerter/ # Notification dispatch
│ │ ├── formatter.py # Alert message formatting
│ │ └── dispatcher.py # Multi-channel delivery
│ └── storage/ # Persistence layer
│ ├── models.py # SQLAlchemy models
│ └── repos.py # Repository pattern
├── tests/ # Test suite ├── tests/ # Test suite
├── scripts/ ├── scripts/
│ └── backtest.py # Historical analysis │ └── backtest.py # Historical analysis
@@ -289,16 +281,16 @@ Contributions are welcome! Please read our Contributing Guide before submitting
```bash ```bash
# Install dev dependencies # Install dev dependencies
pip install -e ".[dev]" uv sync --all-extras
# Run tests # Run tests
pytest uv run pytest
# Run linting # Run linting
ruff check src/ uv run ruff check src/
# Run type checking # Run type checking
mypy src/ uv run mypy src/
``` ```
--- ---
+1 -1
View File
@@ -19,7 +19,7 @@ if config.config_file_name is not None:
target_metadata = Base.metadata target_metadata = Base.metadata
# Get database URL from environment variable or config # Get database URL from environment variable or config
database_url = os.environ.get("SQLALCHEMY_DATABASE_URL") database_url = os.environ.get("DATABASE_URL")
if database_url: if database_url:
config.set_main_option("sqlalchemy.url", database_url) config.set_main_option("sqlalchemy.url", database_url)
@@ -5,16 +5,16 @@ Revises:
Create Date: 2026-01-04 00:00:00.000000+00:00 Create Date: 2026-01-04 00:00:00.000000+00:00
""" """
from typing import Sequence, Union from collections.abc import Sequence
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "001_initial" revision: str = "001_initial"
down_revision: Union[str, None] = None down_revision: str | None = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
-2
View File
@@ -1,5 +1,3 @@
version: "3.8"
services: services:
postgres: postgres:
image: postgres:15 image: postgres:15
@@ -11,7 +11,7 @@ import logging
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum from enum import StrEnum
from redis.asyncio import Redis from redis.asyncio import Redis
@@ -27,7 +27,7 @@ DEFAULT_CACHE_TTL_SECONDS = 600 # 10 minutes
DEFAULT_REDIS_KEY_PREFIX = "polymarket:market:" DEFAULT_REDIS_KEY_PREFIX = "polymarket:market:"
class SyncState(str, Enum): class SyncState(StrEnum):
"""State of the metadata synchronizer.""" """State of the metadata synchronizer."""
STOPPED = "stopped" STOPPED = "stopped"
@@ -86,7 +86,7 @@ class Orderbook:
bids: tuple[OrderbookLevel, ...] bids: tuple[OrderbookLevel, ...]
asks: tuple[OrderbookLevel, ...] asks: tuple[OrderbookLevel, ...]
tick_size: Decimal tick_size: Decimal
timestamp: datetime = field(default_factory=datetime.utcnow) timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod @classmethod
def from_clob_orderbook(cls, orderbook: Any) -> "Orderbook": def from_clob_orderbook(cls, orderbook: Any) -> "Orderbook":
@@ -9,8 +9,9 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any from typing import Any
import websockets
from websockets.asyncio.client import ClientConnection from websockets.asyncio.client import ClientConnection
from websockets.asyncio.client import connect as ws_connect
from websockets.exceptions import ConnectionClosed
from polymarket_insider_tracker.ingestor.models import TradeEvent from polymarket_insider_tracker.ingestor.models import TradeEvent
@@ -156,7 +157,7 @@ class TradeStreamHandler:
await self._set_state(ConnectionState.CONNECTING) await self._set_state(ConnectionState.CONNECTING)
try: try:
ws = await websockets.connect( ws = await ws_connect(
self._host, self._host,
ping_interval=self._ping_interval, ping_interval=self._ping_interval,
ping_timeout=self._ping_interval * 2, ping_timeout=self._ping_interval * 2,
@@ -227,7 +228,7 @@ class TradeStreamHandler:
else: else:
logger.debug("Received binary message (%d bytes)", len(message)) logger.debug("Received binary message (%d bytes)", len(message))
except websockets.ConnectionClosed as e: except ConnectionClosed as e:
logger.warning("Connection closed: %s", e) logger.warning("Connection closed: %s", e)
raise raise
except Exception as e: except Exception as e:
@@ -284,7 +285,7 @@ class TradeStreamHandler:
while self._running: while self._running:
try: try:
await self._listen(self._ws) await self._listen(self._ws)
except (websockets.ConnectionClosed, Exception) as e: except (ConnectionClosed, Exception) as e:
if not self._running: if not self._running:
break break
+2 -2
View File
@@ -11,7 +11,7 @@ import contextlib
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum from enum import StrEnum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from redis.asyncio import Redis from redis.asyncio import Redis
@@ -43,7 +43,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PipelineState(str, Enum): class PipelineState(StrEnum):
"""Pipeline lifecycle states.""" """Pipeline lifecycle states."""
STOPPED = "stopped" STOPPED = "stopped"
+8 -2
View File
@@ -231,7 +231,10 @@ class TestTradeStreamHandler:
mock_ws = AsyncMock() mock_ws = AsyncMock()
mock_ws.send = AsyncMock() mock_ws.send = AsyncMock()
with patch("websockets.connect", AsyncMock(return_value=mock_ws)): with patch(
"polymarket_insider_tracker.ingestor.websocket.ws_connect",
AsyncMock(return_value=mock_ws),
):
ws = await handler._connect() ws = await handler._connect()
assert ws is mock_ws assert ws is mock_ws
@@ -333,7 +336,10 @@ class TestTradeStreamHandlerIntegration:
mock_ws = MockWebSocket(handler, trade_message) mock_ws = MockWebSocket(handler, trade_message)
with patch("websockets.connect", AsyncMock(return_value=mock_ws)): with patch(
"polymarket_insider_tracker.ingestor.websocket.ws_connect",
AsyncMock(return_value=mock_ws),
):
# Run with timeout to prevent hanging # Run with timeout to prevent hanging
try: try:
await asyncio.wait_for(handler.start(), timeout=1.0) await asyncio.wait_for(handler.start(), timeout=1.0)