AJAY KASU commited on
Commit
81146df
·
1 Parent(s): 27279c5

Add tests, alerts, and blockchain tracker

Browse files
src/alerts.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class AlertManager:
8
+ def __init__(self):
9
+ # Read Webhook URLs from environment
10
+ self.discord_webhook = os.environ.get("DISCORD_WEBHOOK_URL")
11
+ self.slack_webhook = os.environ.get("SLACK_WEBHOOK_URL")
12
+
13
+ def send_discord_alert(self, title: str, message: str, color: int = 5814783):
14
+ """Send a rich embed to Discord."""
15
+ if not self.discord_webhook:
16
+ logger.debug("Discord webhook not configured.")
17
+ return
18
+
19
+ payload = {
20
+ "embeds": [{
21
+ "title": title,
22
+ "description": message,
23
+ "color": color
24
+ }]
25
+ }
26
+
27
+ try:
28
+ response = requests.post(self.discord_webhook, json=payload)
29
+ if response.status_code != 204:
30
+ logger.error(f"Failed to send Discord alert: {response.status_code}")
31
+ except Exception as e:
32
+ logger.error(f"Error sending Discord alert: {e}")
33
+
34
+ def send_slack_alert(self, title: str, message: str):
35
+ """Send a message to Slack."""
36
+ if not self.slack_webhook:
37
+ logger.debug("Slack webhook not configured.")
38
+ return
39
+
40
+ payload = {
41
+ "text": f"*{title}*\n{message}"
42
+ }
43
+
44
+ try:
45
+ response = requests.post(self.slack_webhook, json=payload)
46
+ if response.status_code != 200:
47
+ logger.error(f"Failed to send Slack alert: {response.status_code}")
48
+ except Exception as e:
49
+ logger.error(f"Error sending Slack alert: {e}")
50
+
51
+ def alert_arbitrage(self, opp: dict):
52
+ """Format and send an arbitrage opportunity alert."""
53
+ title = "🚨 New Arbitrage Opportunity Detected!"
54
+ msg = f"**{opp['event_name']}**\n"
55
+ msg += f"Buy on {opp['buy_platform']} @ ${opp['buy_price']}\n"
56
+ msg += f"Sell on {opp['sell_platform']} @ ${opp['sell_price']}\n"
57
+ msg += f"**Net Edge:** {opp['expected_profit_margin']*100:.2f}%\n"
58
+ msg += f"**Max Size:** ${opp['buy_size']:.2f}"
59
+
60
+ # Color: Greenish for profit
61
+ self.send_discord_alert(title, msg, color=5763719)
62
+ self.send_slack_alert(title, msg)
src/blockchain.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from web3 import Web3
4
+ from typing import Callable, Optional
5
+ import asyncio
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class BlockchainTracker:
10
+ def __init__(self):
11
+ """
12
+ Track whale movements on Polygon (Polymarket's native chain).
13
+ """
14
+ # Alchemy or Infura RPC URL for Polygon Mainnet
15
+ self.rpc_url = os.environ.get("POLYGON_RPC_URL", "https://polygon-rpc.com")
16
+ self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
17
+
18
+ if self.w3.is_connected():
19
+ logger.info(f"Connected to Polygon Network via {self.rpc_url}")
20
+ else:
21
+ logger.error("Failed to connect to Polygon Network")
22
+
23
+ # USDC Token Contract on Polygon
24
+ self.usdc_address = self.w3.to_checksum_address("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")
25
+
26
+ # Polymarket CTF Exchange Contract
27
+ self.poly_exchange = self.w3.to_checksum_address("0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E")
28
+
29
+ # Standard ERC20 Transfer ABI (reduced form)
30
+ self.erc20_abi = [
31
+ {
32
+ "anonymous": False,
33
+ "inputs": [
34
+ {"indexed": True, "name": "from", "type": "address"},
35
+ {"indexed": True, "name": "to", "type": "address"},
36
+ {"indexed": False, "name": "value", "type": "uint256"}
37
+ ],
38
+ "name": "Transfer",
39
+ "type": "event"
40
+ }
41
+ ]
42
+
43
+ self.usdc_contract = self.w3.eth.contract(address=self.usdc_address, abi=self.erc20_abi)
44
+
45
+ async def watch_whale_deposits(self, threshold_usdc: float = 100000.0, callback: Optional[Callable] = None):
46
+ """
47
+ Poll for large USDC transfers (whales) flowing into Polymarket.
48
+ Threshold defaults to 100k USDC (USDC has 6 decimals on Polygon).
49
+ """
50
+ threshold_wei = int(threshold_usdc * (10 ** 6))
51
+
52
+ # Create a filter for Transfer events to the Polymarket exchange
53
+ # Note: In production, relying on direct get_logs polling can be rate limited.
54
+ # WebSockets (WSS) and async Web3 is preferred.
55
+
56
+ filter_params = {
57
+ 'address': self.usdc_address,
58
+ 'topics': [
59
+ self.w3.keccak(text="Transfer(address,address,uint256)").hex(),
60
+ None, # from any
61
+ "0x000000000000000000000000" + self.poly_exchange[2:] # to polymarket
62
+ ]
63
+ }
64
+
65
+ latest_block = self.w3.eth.block_number
66
+
67
+ while True:
68
+ try:
69
+ # Fetch logs from latest block
70
+ logs = self.w3.eth.get_logs({**filter_params, 'fromBlock': latest_block, 'toBlock': 'latest'})
71
+
72
+ for log in logs:
73
+ # value is not indexed, so it's in the data field
74
+ value = int(log['data'].hex(), 16)
75
+
76
+ if value >= threshold_wei:
77
+ usdc_amount = value / (10 ** 6)
78
+ sender = "0x" + log['topics'][1].hex()[-40:]
79
+ logger.info(f"🐋 WHALE ALERT: {usdc_amount} USDC deposited to Polymarket by {sender}")
80
+
81
+ if callback:
82
+ callback(f"🐋 WHALE ALERT", f"${usdc_amount:,.2f} deposited to Polymarket by `{sender}`")
83
+
84
+ latest_block = self.w3.eth.block_number + 1
85
+
86
+ except Exception as e:
87
+ logger.error(f"Blockchain poll error: {e}")
88
+
89
+ await asyncio.sleep(15) # Poll every 15 seconds (Polygon block time is ~2s)
tests/__init__.py ADDED
File without changes
tests/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (143 Bytes). View file
 
tests/__pycache__/test_arbitrage.cpython-39-pytest-7.1.1.pyc ADDED
Binary file (4.33 kB). View file
 
tests/__pycache__/test_bayesian.cpython-39-pytest-7.1.1.pyc ADDED
Binary file (3.46 kB). View file
 
tests/__pycache__/test_kalman.cpython-39-pytest-7.1.1.pyc ADDED
Binary file (2.5 kB). View file
 
tests/test_arbitrage.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from src.strategies.arbitrage import CrossPlatformArbitrage, IntraMarketArbitrage
3
+
4
+ def test_cross_platform_arbitrage_profitable():
5
+ scanner = CrossPlatformArbitrage(min_profit_threshold=0.01)
6
+
7
+ # State where Arb exists (Polymarket cheaper than Kalshi)
8
+ scanner.update_state('polymarket', '0x217...', bid=0.50, bid_size=1000, ask=0.52, ask_size=1000)
9
+ scanner.update_state('kalshi', 'KXUS2024', bid=0.55, bid_size=1500, ask=0.57, ask_size=500)
10
+
11
+ opps = scanner.scan_opportunities()
12
+
13
+ assert len(opps) == 1
14
+ opp = opps[0]
15
+
16
+ assert opp.buy_platform == 'polymarket'
17
+ assert opp.sell_platform == 'kalshi'
18
+ assert opp.buy_price == 0.52
19
+ assert opp.sell_price == 0.55
20
+ assert opp.buy_size == 1000 # min of 1000 and 1500
21
+ assert opp.expected_profit_margin > 0.01 # Expect profit > min threshold
22
+
23
+ def test_cross_platform_arbitrage_no_opportunity():
24
+ scanner = CrossPlatformArbitrage(min_profit_threshold=0.01)
25
+
26
+ # Efficient market state
27
+ scanner.update_state('polymarket', '0x217...', bid=0.53, bid_size=1000, ask=0.54, ask_size=1000)
28
+ scanner.update_state('kalshi', 'KXUS2024', bid=0.53, bid_size=1500, ask=0.54, ask_size=500)
29
+
30
+ opps = scanner.scan_opportunities()
31
+ assert len(opps) == 0
32
+
33
+ def test_intra_market_arbitrage():
34
+ scanner = IntraMarketArbitrage(min_profit_threshold=0.01)
35
+
36
+ # Parity violation: P(Yes) + P(No) = 0.45 + 0.45 = 0.90 < 1.0 (Margin = 0.10)
37
+ margin = scanner.check_parity_violation(ask_yes=0.45, ask_no=0.45)
38
+
39
+ assert margin is not None
40
+ assert round(margin, 2) == 0.10
41
+
42
+ # Efficient market: P(Yes) + P(No) = 0.52 + 0.52 = 1.04 > 1.0 (No Arb)
43
+ margin_eff = scanner.check_parity_violation(ask_yes=0.52, ask_no=0.52)
44
+ assert margin_eff is None
tests/test_bayesian.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from src.models.bayesian import BayesianFairValue
3
+
4
+ def test_bayesian_fair_value_initialization():
5
+ model = BayesianFairValue(prior_prob=0.5, prior_confidence=10.0)
6
+
7
+ assert model.get_fair_value() == 0.5
8
+ assert model.alpha == 5.0
9
+ assert model.beta == 5.0
10
+
11
+ def test_bayesian_update():
12
+ model = BayesianFairValue(prior_prob=0.5, prior_confidence=10.0)
13
+
14
+ # We observe a lot of volume trading at 0.70 implied probability
15
+ model.update(market_implied_prob=0.70, trade_volume=100.0, noise_factor=0.1)
16
+
17
+ # FV should shift towards 0.70
18
+ fv = model.get_fair_value()
19
+ assert fv > 0.5
20
+ assert fv < 0.70
21
+
22
+ def test_evaluate_opportunity():
23
+ model = BayesianFairValue(prior_prob=0.6, prior_confidence=100.0) # Highly confident at 0.6
24
+
25
+ # Market asks are way below FV (Underpriced)
26
+ opp_buy = model.evaluate_opportunity(current_ask=0.40, current_bid=0.38)
27
+ assert opp_buy is not None
28
+ assert opp_buy["action"] == "BUY_YES"
29
+ assert opp_buy["edge"] > 0.15 # 0.60 - 0.40 = 0.20ish edge
30
+
31
+ # Market bids are way above FV (Overpriced)
32
+ opp_sell = model.evaluate_opportunity(current_ask=0.82, current_bid=0.80)
33
+ assert opp_sell is not None
34
+ assert opp_sell["action"] == "SELL_YES"
35
+ assert opp_sell["edge"] > 0.15 # 0.80 - 0.60 = 0.20ish edge
tests/test_kalman.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import numpy as np
3
+ from src.models.kalman import KalmanPriceSmoother
4
+
5
+ def test_kalman_filter_initialization():
6
+ filter = KalmanPriceSmoother()
7
+ assert filter.x is None
8
+
9
+ filter.initialize(0.50)
10
+ assert filter.x == 0.50
11
+ assert filter.P == 1.0
12
+
13
+ def test_kalman_filter_smoothing():
14
+ filter = KalmanPriceSmoother(process_variance=1e-4, measurement_variance=1e-2)
15
+
16
+ # Simulate a stable price with a sudden noisy spike
17
+ prices = [0.50, 0.505, 0.495, 0.50, 0.51, 0.65, 0.50, 0.505]
18
+
19
+ smoothed = filter.batch_smooth(np.array(prices))
20
+
21
+ # The smoothed value at index 5 (0.65 spike) should be significantly lower than 0.65
22
+ # because the filter heavily trusts its prior state (0.50) and dampens the noise.
23
+ assert smoothed[5] < 0.65
24
+ assert smoothed[5] > 0.50 # It should move up slightly
25
+
26
+ # The final smoothed value should quickly return near 0.50
27
+ assert abs(smoothed[-1] - 0.50) < 0.05