analysis-poly 0.1.0__tar.gz → 0.1.2__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 (43) hide show
  1. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/PKG-INFO +5 -5
  2. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/README.md +4 -4
  3. analysis_poly-0.1.2/analysis_poly/activity_discovery.py +230 -0
  4. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/analyzer.py +336 -124
  5. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/main.py +1 -1
  6. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/models.py +40 -23
  7. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/open_with_params.py +3 -9
  8. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/polymarket_client.py +34 -2
  9. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/profit_engine.py +169 -56
  10. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/run_manager.py +140 -5
  11. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/static/dist/app.css +1 -1
  12. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/static/dist/app.js +96 -96
  13. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/templates/index.html +1 -1
  14. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/web.py +14 -3
  15. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/PKG-INFO +5 -5
  16. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/SOURCES.txt +4 -0
  17. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/pyproject.toml +2 -1
  18. analysis_poly-0.1.2/tests/test_analyzer_csv.py +72 -0
  19. analysis_poly-0.1.2/tests/test_analyzer_discovery.py +228 -0
  20. analysis_poly-0.1.2/tests/test_analyzer_result_cache_compat.py +56 -0
  21. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_models_defaults.py +10 -6
  22. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_open_with_params.py +3 -7
  23. analysis_poly-0.1.2/tests/test_profit_engine.py +475 -0
  24. analysis_poly-0.1.2/tests/test_run_manager.py +145 -0
  25. analysis_poly-0.1.0/tests/test_profit_engine.py +0 -173
  26. analysis_poly-0.1.0/tests/test_run_manager.py +0 -75
  27. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/__init__.py +0 -0
  28. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/cli.py +0 -0
  29. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/logging_config.py +0 -0
  30. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/market_cache.py +0 -0
  31. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/market_result_cache.py +0 -0
  32. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/slugs.py +0 -0
  33. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/dependency_links.txt +0 -0
  34. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/entry_points.txt +0 -0
  35. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/requires.txt +0 -0
  36. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/top_level.txt +0 -0
  37. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/setup.cfg +0 -0
  38. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_chunking.py +0 -0
  39. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_filter_empty_market.py +0 -0
  40. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_market_cache.py +0 -0
  41. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_market_cache.py +0 -0
  42. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_market_result_cache.py +0 -0
  43. {analysis_poly-0.1.0 → analysis_poly-0.1.2}/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.0
3
+ Version: 0.1.2
4
4
  Summary: Polymarket real-profit web analyzer
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -27,7 +27,8 @@ Polymarket real-profit analyzer with a web UI.
27
27
 
28
28
  ## Scope (Important)
29
29
 
30
- - This tool is primarily designed for Polymarket crypto `updown` markets with taker fees, especially `5m` and `15m` intervals.
30
+ - This tool is primarily designed for Polymarket crypto `updown` markets with taker fees.
31
+ - The dashboard first discovers participated slugs from Polymarket user activity within the selected time range, then replays only the matched markets.
31
32
  - It is **not** intended as a universal PnL engine for all Polymarket market types.
32
33
  - Main purpose: quantify and visualize the impact of trading fees on real profitability (`Net PnL` vs `No-Fee PnL`).
33
34
 
@@ -48,7 +49,7 @@ After install:
48
49
 
49
50
  ```bash
50
51
  analysis-poly
51
- analysis-poly-open --address 0xabc --symbols btc,eth --intervals 5,15
52
+ analysis-poly-open --address 0xabc --keywords updown,15m
52
53
  ```
53
54
 
54
55
  ## Run
@@ -67,8 +68,7 @@ Use a standalone script to start server, open browser, and pass params in URL.
67
68
  ```bash
68
69
  uv run -m analysis_poly.open_with_params \
69
70
  --address 0xabc \
70
- --symbols btc,eth \
71
- --intervals 5,15 \
71
+ --keywords updown,15m \
72
72
  --start-time "2026-03-01 00:00" \
73
73
  --end-time "2026-03-02 00:00" \
74
74
  --concurrency 8
@@ -11,7 +11,8 @@ Polymarket real-profit analyzer with a web UI.
11
11
 
12
12
  ## Scope (Important)
13
13
 
14
- - This tool is primarily designed for Polymarket crypto `updown` markets with taker fees, especially `5m` and `15m` intervals.
14
+ - This tool is primarily designed for Polymarket crypto `updown` markets with taker fees.
15
+ - The dashboard first discovers participated slugs from Polymarket user activity within the selected time range, then replays only the matched markets.
15
16
  - It is **not** intended as a universal PnL engine for all Polymarket market types.
16
17
  - Main purpose: quantify and visualize the impact of trading fees on real profitability (`Net PnL` vs `No-Fee PnL`).
17
18
 
@@ -32,7 +33,7 @@ After install:
32
33
 
33
34
  ```bash
34
35
  analysis-poly
35
- analysis-poly-open --address 0xabc --symbols btc,eth --intervals 5,15
36
+ analysis-poly-open --address 0xabc --keywords updown,15m
36
37
  ```
37
38
 
38
39
  ## Run
@@ -51,8 +52,7 @@ Use a standalone script to start server, open browser, and pass params in URL.
51
52
  ```bash
52
53
  uv run -m analysis_poly.open_with_params \
53
54
  --address 0xabc \
54
- --symbols btc,eth \
55
- --intervals 5,15 \
55
+ --keywords updown,15m \
56
56
  --start-time "2026-03-01 00:00" \
57
57
  --end-time "2026-03-02 00:00" \
58
58
  --concurrency 8
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+
6
+ from .models import WarningItem
7
+
8
+ DISCOVERY_ACTIVITY_TYPES = ("TRADE", "SPLIT", "REDEEM")
9
+ DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX = 500
10
+ DISCOVERY_ACTIVITY_OFFSET_MAX = 10000
11
+ SECONDS_PER_DAY = 24 * 60 * 60
12
+
13
+
14
+ @dataclass
15
+ class DiscoveredMarket:
16
+ slug: str
17
+ condition_id: str
18
+ first_activity_ts: int
19
+ last_activity_ts: int
20
+
21
+
22
+ async def collect_user_activity(
23
+ client,
24
+ address: str,
25
+ start_ts: int,
26
+ end_ts: int,
27
+ page_limit: int,
28
+ warnings: list[WarningItem],
29
+ activity_types: list[str] | tuple[str, ...] | None = None,
30
+ ) -> list:
31
+ records: list = []
32
+ offset = 0
33
+ reached_offset_cap = False
34
+
35
+ while True:
36
+ page = await client.get_user_activity_page(
37
+ user=address,
38
+ activity_types=list(activity_types or DISCOVERY_ACTIVITY_TYPES),
39
+ start_ts=start_ts,
40
+ end_ts=end_ts,
41
+ limit=page_limit,
42
+ offset=offset,
43
+ sort_direction="ASC",
44
+ )
45
+ if not page:
46
+ break
47
+ records.extend(page)
48
+ if len(page) < page_limit:
49
+ break
50
+ offset += len(page)
51
+ if offset > DISCOVERY_ACTIVITY_OFFSET_MAX:
52
+ reached_offset_cap = True
53
+ break
54
+
55
+ if not reached_offset_cap:
56
+ return dedupe_activity_records(records)
57
+
58
+ if end_ts - start_ts <= 1:
59
+ warnings.append(
60
+ WarningItem(
61
+ timestamp=start_ts,
62
+ code="DISCOVERY_WINDOW_TRUNCATED",
63
+ message="user activity window is too dense to split further; discovery may be incomplete",
64
+ )
65
+ )
66
+ return dedupe_activity_records(records)
67
+
68
+ split_ts = start_ts + ((end_ts - start_ts) // 2)
69
+ left_records, right_records = await asyncio.gather(
70
+ collect_user_activity(
71
+ client=client,
72
+ address=address,
73
+ start_ts=start_ts,
74
+ end_ts=split_ts,
75
+ page_limit=page_limit,
76
+ warnings=warnings,
77
+ activity_types=activity_types,
78
+ ),
79
+ collect_user_activity(
80
+ client=client,
81
+ address=address,
82
+ start_ts=split_ts + 1,
83
+ end_ts=end_ts,
84
+ page_limit=page_limit,
85
+ warnings=warnings,
86
+ activity_types=activity_types,
87
+ ),
88
+ )
89
+ return dedupe_activity_records([*left_records, *right_records])
90
+
91
+
92
+ async def discover_user_markets_in_range(
93
+ client,
94
+ address: str,
95
+ start_ts: int,
96
+ end_ts: int,
97
+ page_limit: int,
98
+ warnings: list[WarningItem],
99
+ ) -> list[DiscoveredMarket]:
100
+ records = await collect_user_activity(
101
+ client=client,
102
+ address=address,
103
+ start_ts=start_ts,
104
+ end_ts=end_ts,
105
+ page_limit=min(max(1, page_limit), DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX),
106
+ warnings=warnings,
107
+ activity_types=DISCOVERY_ACTIVITY_TYPES,
108
+ )
109
+ return summarize_discovered_markets(records, warnings)
110
+
111
+
112
+ async def discover_user_markets_by_day(
113
+ client,
114
+ address: str,
115
+ start_ts: int,
116
+ end_ts: int,
117
+ page_limit: int,
118
+ warnings: list[WarningItem],
119
+ ) -> list[DiscoveredMarket]:
120
+ market_by_key: dict[str, DiscoveredMarket] = {}
121
+ for window_start, window_end in iter_day_windows(start_ts, end_ts):
122
+ day_records = await collect_user_activity(
123
+ client=client,
124
+ address=address,
125
+ start_ts=window_start,
126
+ end_ts=window_end,
127
+ page_limit=min(max(1, page_limit), DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX),
128
+ warnings=warnings,
129
+ activity_types=DISCOVERY_ACTIVITY_TYPES,
130
+ )
131
+ for discovered in summarize_discovered_markets(day_records, warnings):
132
+ market_key = discovered.condition_id or discovered.slug
133
+ existing = market_by_key.get(market_key)
134
+ if existing is None:
135
+ market_by_key[market_key] = discovered
136
+ continue
137
+ existing.first_activity_ts = min(existing.first_activity_ts, discovered.first_activity_ts)
138
+ existing.last_activity_ts = max(existing.last_activity_ts, discovered.last_activity_ts)
139
+ if not existing.slug and discovered.slug:
140
+ existing.slug = discovered.slug
141
+
142
+ return sorted(
143
+ market_by_key.values(),
144
+ key=lambda item: (item.first_activity_ts, item.last_activity_ts, item.slug),
145
+ )
146
+
147
+
148
+ def summarize_discovered_markets(records: list, warnings: list[WarningItem]) -> list[DiscoveredMarket]:
149
+ market_by_key: dict[str, DiscoveredMarket] = {}
150
+ warned_missing_slug: set[str] = set()
151
+
152
+ for record in records:
153
+ slug = str(record.slug or "").strip()
154
+ condition_id = str(record.condition_id or "").strip()
155
+ if not slug:
156
+ dedupe_key = f"{condition_id}:{record.type}"
157
+ if dedupe_key not in warned_missing_slug:
158
+ warnings.append(
159
+ WarningItem(
160
+ timestamp=record.timestamp,
161
+ code="DISCOVERY_SKIP_MISSING_SLUG",
162
+ message="skip user activity record without market slug",
163
+ )
164
+ )
165
+ warned_missing_slug.add(dedupe_key)
166
+ continue
167
+
168
+ market_key = condition_id or slug
169
+ existing = market_by_key.get(market_key)
170
+ if existing is None:
171
+ market_by_key[market_key] = DiscoveredMarket(
172
+ slug=slug,
173
+ condition_id=condition_id,
174
+ first_activity_ts=record.timestamp,
175
+ last_activity_ts=record.timestamp,
176
+ )
177
+ continue
178
+
179
+ existing.first_activity_ts = min(existing.first_activity_ts, record.timestamp)
180
+ existing.last_activity_ts = max(existing.last_activity_ts, record.timestamp)
181
+ if not existing.slug and slug:
182
+ existing.slug = slug
183
+
184
+ return sorted(
185
+ market_by_key.values(),
186
+ key=lambda item: (item.first_activity_ts, item.last_activity_ts, item.slug),
187
+ )
188
+
189
+
190
+ def dedupe_activity_records(records: list) -> list:
191
+ deduped: dict[str, object] = {}
192
+ for record in records:
193
+ deduped[activity_key(record)] = record
194
+ return sorted(
195
+ deduped.values(),
196
+ key=lambda item: (
197
+ int(getattr(item, "timestamp", 0)),
198
+ str(getattr(item, "transaction_hash", "")),
199
+ str(getattr(item, "type", "")),
200
+ ),
201
+ )
202
+
203
+
204
+ def activity_key(record) -> str:
205
+ return "|".join(
206
+ [
207
+ str(getattr(record, "transaction_hash", "")),
208
+ str(getattr(record, "condition_id", "")),
209
+ str(getattr(record, "type", "")),
210
+ str(getattr(record, "timestamp", 0)),
211
+ str(getattr(record, "slug", "")),
212
+ str(getattr(record, "size", 0)),
213
+ str(getattr(record, "usdc_size", 0)),
214
+ ]
215
+ )
216
+
217
+
218
+ def iter_day_windows(start_ts: int, end_ts: int) -> list[tuple[int, int]]:
219
+ if start_ts > end_ts:
220
+ return []
221
+
222
+ windows: list[tuple[int, int]] = []
223
+ current = start_ts
224
+ while current <= end_ts:
225
+ day_start = (current // SECONDS_PER_DAY) * SECONDS_PER_DAY
226
+ day_end = day_start + SECONDS_PER_DAY - 1
227
+ window_end = min(day_end, end_ts)
228
+ windows.append((current, window_end))
229
+ current = window_end + 1
230
+ return windows