analysis-poly 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/PKG-INFO +1 -1
  2. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/activity_discovery.py +11 -0
  3. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/analyzer.py +17 -1
  4. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/profit_engine.py +7 -2
  5. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/PKG-INFO +1 -1
  6. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/pyproject.toml +1 -1
  7. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_discovery.py +135 -0
  8. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_profit_engine.py +46 -0
  9. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/README.md +0 -0
  10. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/__init__.py +0 -0
  11. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/cli.py +0 -0
  12. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/logging_config.py +0 -0
  13. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/main.py +0 -0
  14. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/market_cache.py +0 -0
  15. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/market_result_cache.py +0 -0
  16. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/models.py +0 -0
  17. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/open_with_params.py +0 -0
  18. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/polymarket_client.py +0 -0
  19. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/run_manager.py +0 -0
  20. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/slugs.py +0 -0
  21. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/static/dist/app.css +0 -0
  22. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/static/dist/app.js +0 -0
  23. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/templates/index.html +0 -0
  24. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly/web.py +0 -0
  25. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/SOURCES.txt +0 -0
  26. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/dependency_links.txt +0 -0
  27. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/entry_points.txt +0 -0
  28. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/requires.txt +0 -0
  29. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/analysis_poly.egg-info/top_level.txt +0 -0
  30. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/setup.cfg +0 -0
  31. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_chunking.py +0 -0
  32. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_csv.py +0 -0
  33. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_filter_empty_market.py +0 -0
  34. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_market_cache.py +0 -0
  35. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_analyzer_result_cache_compat.py +0 -0
  36. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_market_cache.py +0 -0
  37. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_market_result_cache.py +0 -0
  38. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_models_defaults.py +0 -0
  39. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_open_with_params.py +0 -0
  40. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_run_manager.py +0 -0
  41. {analysis_poly-0.1.2 → analysis_poly-0.1.3}/tests/test_slugs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: analysis-poly
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Polymarket real-profit web analyzer
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -150,6 +150,9 @@ def summarize_discovered_markets(records: list, warnings: list[WarningItem]) ->
150
150
  warned_missing_slug: set[str] = set()
151
151
 
152
152
  for record in records:
153
+ if _is_zero_value_redeem(record):
154
+ continue
155
+
153
156
  slug = str(record.slug or "").strip()
154
157
  condition_id = str(record.condition_id or "").strip()
155
158
  if not slug:
@@ -187,6 +190,14 @@ def summarize_discovered_markets(records: list, warnings: list[WarningItem]) ->
187
190
  )
188
191
 
189
192
 
193
+ def _is_zero_value_redeem(record) -> bool:
194
+ if str(getattr(record, "type", "")).upper() != "REDEEM":
195
+ return False
196
+ size = float(getattr(record, "size", 0) or 0)
197
+ usdc_size = float(getattr(record, "usdc_size", 0) or 0)
198
+ return size <= 0 and usdc_size <= 0
199
+
200
+
190
201
  def dedupe_activity_records(records: list) -> list:
191
202
  deduped: dict[str, object] = {}
192
203
  for record in records:
@@ -147,6 +147,8 @@ class PolymarketProfitAnalyzer:
147
147
  )
148
148
  discovered_markets = _filter_discovered_markets(
149
149
  discovered_markets,
150
+ start_ts=req.start_ts,
151
+ end_ts=req.end_ts,
150
152
  keywords=req.keywords,
151
153
  )
152
154
  total_markets = len(discovered_markets)
@@ -783,9 +785,23 @@ def _is_market_result_cache_eligible(slug: str, now_ts: int, recent_window_sec:
783
785
 
784
786
  def _filter_discovered_markets(
785
787
  discovered_markets: list[DiscoveredMarket],
788
+ start_ts: int,
789
+ end_ts: int,
786
790
  keywords: list[str],
787
791
  ) -> list[DiscoveredMarket]:
788
- return [item for item in discovered_markets if _slug_matches_filters(item.slug, keywords=keywords)]
792
+ return [
793
+ item
794
+ for item in discovered_markets
795
+ if _slug_in_time_range(item.slug, start_ts=start_ts, end_ts=end_ts)
796
+ and _slug_matches_filters(item.slug, keywords=keywords)
797
+ ]
798
+
799
+
800
+ def _slug_in_time_range(slug: str, start_ts: int, end_ts: int) -> bool:
801
+ market_ts = _market_ts(slug)
802
+ if market_ts is None:
803
+ return True
804
+ return start_ts <= market_ts <= end_ts
789
805
 
790
806
 
791
807
  def _slug_matches_filters(slug: str, keywords: list[str]) -> bool:
@@ -128,6 +128,11 @@ class ProfitEngine:
128
128
  )
129
129
 
130
130
  for redeem in redeem_activities:
131
+ redeem_size = float(redeem.size)
132
+ redeem_usdc_size = float(redeem.usdc_size)
133
+ if redeem_size <= 0 and redeem_usdc_size <= 0:
134
+ continue
135
+
131
136
  winner_token = _resolve_winner_token(market)
132
137
  if not winner_token:
133
138
  warnings.append(
@@ -146,8 +151,8 @@ class ProfitEngine:
146
151
  tx=redeem.transaction_hash,
147
152
  kind="REDEEM",
148
153
  token_id=winner_token,
149
- size=float(redeem.size),
150
- usdc_size=float(redeem.usdc_size),
154
+ size=redeem_size,
155
+ usdc_size=redeem_usdc_size,
151
156
  )
152
157
  )
153
158
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: analysis-poly
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Polymarket real-profit web analyzer
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "analysis-poly"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Polymarket real-profit web analyzer"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
 
3
+ from analysis_poly.activity_discovery import summarize_discovered_markets
3
4
  from analysis_poly.analyzer import PolymarketProfitAnalyzer
4
5
  from analysis_poly.models import ActivityRecord, AnalysisRequest, MarketReport, PolymarketMarket, TokenReport
5
6
  from analysis_poly.profit_engine import PnlDelta
@@ -122,6 +123,107 @@ def test_run_discovers_daily_markets_and_filters_keywords(monkeypatch):
122
123
  asyncio.run(runner())
123
124
 
124
125
 
126
+ def test_run_filters_discovered_markets_by_slug_timestamp(monkeypatch):
127
+ class FakeClient:
128
+ async def get_user_activity_page(
129
+ self,
130
+ user,
131
+ activity_types=None,
132
+ start_ts=None,
133
+ end_ts=None,
134
+ limit=500,
135
+ offset=0,
136
+ sort_direction="ASC",
137
+ ):
138
+ activity_key = tuple(activity_types or [])
139
+ if activity_key == ("TRADE", "SPLIT", "REDEEM"):
140
+ return [
141
+ ActivityRecord.model_validate(
142
+ {
143
+ "transactionHash": "0xold_redeem",
144
+ "timestamp": 1776432712,
145
+ "type": "REDEEM",
146
+ "conditionId": "cond_old",
147
+ "slug": "btc-updown-15m-1767960900",
148
+ "size": 1,
149
+ "usdcSize": 1,
150
+ }
151
+ ),
152
+ ActivityRecord.model_validate(
153
+ {
154
+ "transactionHash": "0xlive_trade",
155
+ "timestamp": 1776432800,
156
+ "type": "TRADE",
157
+ "conditionId": "cond_live",
158
+ "slug": "btc-updown-15m-1776432800",
159
+ "size": 1,
160
+ "usdcSize": 0.5,
161
+ }
162
+ ),
163
+ ]
164
+ return []
165
+
166
+ async def aclose(self):
167
+ return
168
+
169
+ fetched_slugs = []
170
+
171
+ async def fake_fetch_markets_with_status(_client, slugs, concurrency):
172
+ fetched_slugs.extend(slugs)
173
+ market = PolymarketMarket(
174
+ slug="btc-updown-15m-1776432800",
175
+ condition_id="cond_live",
176
+ up_token_id="up_live",
177
+ down_token_id="down_live",
178
+ outcomes=["Up", "Down"],
179
+ outcome_prices=[0.5, 0.5],
180
+ )
181
+ return [(slug, market if slug == market.slug else None) for slug in slugs]
182
+
183
+ async def fake_process_single_market(client, engine, engine_no_fee, address, address_market_cache, req, market):
184
+ report = MarketReport(
185
+ market_slug=market.slug,
186
+ condition_id=market.condition_id,
187
+ up_token_id=market.up_token_id,
188
+ down_token_id=market.down_token_id,
189
+ realized_pnl_usdc=1.0,
190
+ tokens=[TokenReport(token_id=market.up_token_id, outcome="Up", realized_pnl_usdc=1.0, trade_count=1)],
191
+ )
192
+ delta = PnlDelta(timestamp=1776432800, market_slug=market.slug, token_id=market.up_token_id, delta_pnl_usdc=1.0)
193
+ from analysis_poly.analyzer import _MarketProcessResult
194
+
195
+ return _MarketProcessResult(
196
+ market_slug=market.slug,
197
+ market_report=report,
198
+ market_report_no_fee=report,
199
+ deltas=[delta],
200
+ deltas_no_fee=[delta],
201
+ warnings=[],
202
+ )
203
+
204
+ async def runner():
205
+ monkeypatch.setattr("analysis_poly.analyzer.PolymarketApiClient", lambda timeout_sec=20: FakeClient())
206
+ analyzer = PolymarketProfitAnalyzer()
207
+ monkeypatch.setattr(analyzer, "_fetch_markets_with_status", fake_fetch_markets_with_status)
208
+ monkeypatch.setattr(analyzer, "_process_single_market", fake_process_single_market)
209
+
210
+ report = await analyzer.run(
211
+ AnalysisRequest(
212
+ address="0xb4ef1257f7ac40c26f4dfaaad69ed75f6458682f",
213
+ start_ts=1774972800,
214
+ end_ts=1776679380,
215
+ keywords=[],
216
+ page_limit=1000,
217
+ concurrency=5,
218
+ )
219
+ )
220
+
221
+ assert fetched_slugs == ["btc-updown-15m-1776432800"]
222
+ assert [market.market_slug for market in report.markets] == ["btc-updown-15m-1776432800"]
223
+
224
+ asyncio.run(runner())
225
+
226
+
125
227
  def test_run_adds_daily_maker_rebate_to_summary_and_total_curve(monkeypatch):
126
228
  class FakeClient:
127
229
  async def get_user_activity_page(
@@ -226,3 +328,36 @@ def test_run_adds_daily_maker_rebate_to_summary_and_total_curve(monkeypatch):
226
328
  assert [item.model_dump() for item in report.maker_rebates] == [{"timestamp": 200, "usdc_size": 23.3977}]
227
329
 
228
330
  asyncio.run(runner())
331
+
332
+
333
+ def test_discovery_ignores_zero_value_redeem_activity():
334
+ warnings = []
335
+ records = [
336
+ ActivityRecord.model_validate(
337
+ {
338
+ "transactionHash": "0xzero",
339
+ "timestamp": 1776432712,
340
+ "type": "REDEEM",
341
+ "conditionId": "cond_old",
342
+ "slug": "btc-updown-15m-1767960900",
343
+ "size": 0,
344
+ "usdcSize": 0,
345
+ }
346
+ ),
347
+ ActivityRecord.model_validate(
348
+ {
349
+ "transactionHash": "0xtrade",
350
+ "timestamp": 1776432800,
351
+ "type": "TRADE",
352
+ "conditionId": "cond_live",
353
+ "slug": "btc-updown-15m-1776432800",
354
+ "size": 1,
355
+ "usdcSize": 0.5,
356
+ }
357
+ ),
358
+ ]
359
+
360
+ discovered = summarize_discovered_markets(records, warnings)
361
+
362
+ assert [item.slug for item in discovered] == ["btc-updown-15m-1776432800"]
363
+ assert warnings == []
@@ -438,6 +438,52 @@ def test_closed_market_without_redeem_losing_position_counts_as_loss():
438
438
  assert not any(w.code == "CLOSED_MARKET_UNKNOWN_OUTCOME" for w in warnings)
439
439
 
440
440
 
441
+ def test_zero_value_redeem_does_not_shift_closed_market_settlement_timestamp():
442
+ market = PolymarketMarket(
443
+ slug="btc-updown-15m-1767960900",
444
+ condition_id="cond_zero_redeem",
445
+ up_token_id="up_token",
446
+ down_token_id="down_token",
447
+ outcomes=["Up", "Down"],
448
+ outcome_prices=[0, 1],
449
+ closed=True,
450
+ )
451
+
452
+ taker_buy = TradeRecord.model_validate(
453
+ {
454
+ "transactionHash": "0xbuy",
455
+ "timestamp": 1767960918,
456
+ "side": "BUY",
457
+ "asset": "up_token",
458
+ "conditionId": "cond_zero_redeem",
459
+ "size": 10,
460
+ "price": 0.52,
461
+ }
462
+ )
463
+ zero_redeem = ActivityRecord.model_validate(
464
+ {
465
+ "transactionHash": "0xzero",
466
+ "timestamp": 1776432712,
467
+ "type": "REDEEM",
468
+ "conditionId": "cond_zero_redeem",
469
+ "size": 0,
470
+ "usdcSize": 0,
471
+ }
472
+ )
473
+
474
+ engine = ProfitEngine(fee_rate_bps=1000, missing_cost_warn_qty=0.5)
475
+ report, deltas, _ = engine.process_market(
476
+ market=market,
477
+ taker_trades=[taker_buy],
478
+ all_trades=[taker_buy],
479
+ split_activities=[],
480
+ redeem_activities=[zero_redeem],
481
+ )
482
+
483
+ assert report.realized_pnl_usdc == -5.2
484
+ assert [delta.timestamp for delta in deltas] == [1767960918]
485
+
486
+
441
487
  def test_closed_market_without_resolved_outcome_prices_warns_and_keeps_position():
442
488
  market = PolymarketMarket(
443
489
  slug="btc-updown-5m-4000",
File without changes
File without changes