synpath 0.1.0__tar.gz → 0.2.0__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 (197) hide show
  1. {synpath-0.1.0 → synpath-0.2.0}/.env.example +8 -0
  2. {synpath-0.1.0 → synpath-0.2.0}/PKG-INFO +30 -28
  3. {synpath-0.1.0 → synpath-0.2.0}/README.md +28 -26
  4. {synpath-0.1.0 → synpath-0.2.0}/docs/api.md +1 -1
  5. {synpath-0.1.0 → synpath-0.2.0}/docs/capabilities.md +38 -29
  6. {synpath-0.1.0 → synpath-0.2.0}/docs/engine.md +6 -2
  7. {synpath-0.1.0 → synpath-0.2.0}/docs/streaming.md +15 -0
  8. {synpath-0.1.0 → synpath-0.2.0}/docs/trading.md +30 -0
  9. {synpath-0.1.0 → synpath-0.2.0}/pyproject.toml +2 -2
  10. {synpath-0.1.0 → synpath-0.2.0}/synpath/__init__.py +7 -2
  11. {synpath-0.1.0 → synpath-0.2.0}/synpath/client.py +6 -1
  12. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/__main__.py +1 -0
  13. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/feeds.py +7 -0
  14. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/routed.py +39 -9
  15. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/router.py +97 -20
  16. {synpath-0.1.0 → synpath-0.2.0}/synpath/ids.py +4 -4
  17. {synpath-0.1.0 → synpath-0.2.0}/synpath/matching.py +10 -3
  18. synpath-0.2.0/synpath/opinion.py +799 -0
  19. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/serve.py +1 -1
  20. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/__init__.py +4 -0
  21. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/__main__.py +7 -1
  22. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/credentials.py +45 -1
  23. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/init.py +17 -0
  24. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/instruments.py +5 -0
  25. synpath-0.2.0/synpath/trading/opinion.py +760 -0
  26. synpath-0.2.0/synpath/trading/opinion_signing.py +163 -0
  27. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/types.py +7 -0
  28. {synpath-0.1.0 → synpath-0.2.0}/synpath/types.py +15 -1
  29. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/__init__.py +4 -0
  30. synpath-0.2.0/synpath/ws/opinion.py +459 -0
  31. {synpath-0.1.0 → synpath-0.2.0}/tests/conftest.py +25 -0
  32. synpath-0.2.0/tests/samples/opinion_book.json +1 -0
  33. synpath-0.2.0/tests/samples/opinion_book_no.json +1 -0
  34. synpath-0.2.0/tests/samples/opinion_categorical.json +1 -0
  35. synpath-0.2.0/tests/samples/opinion_child.json +1 -0
  36. synpath-0.2.0/tests/samples/opinion_fee_rate_call.json +1 -0
  37. synpath-0.2.0/tests/samples/opinion_latest_price.json +1 -0
  38. synpath-0.2.0/tests/samples/opinion_market.json +1 -0
  39. synpath-0.2.0/tests/samples/opinion_markets.json +1 -0
  40. synpath-0.2.0/tests/samples/opinion_not_found.json +1 -0
  41. synpath-0.2.0/tests/samples/opinion_prices_1d.json +1 -0
  42. synpath-0.2.0/tests/samples/opinion_prices_1h.json +1 -0
  43. synpath-0.2.0/tests/samples/opinion_resolved.json +1 -0
  44. synpath-0.2.0/tests/samples/opinion_signing_vectors.json +96 -0
  45. {synpath-0.1.0 → synpath-0.2.0}/tests/test_contract.py +24 -1
  46. {synpath-0.1.0 → synpath-0.2.0}/tests/test_hosted.py +19 -0
  47. {synpath-0.1.0 → synpath-0.2.0}/tests/test_init.py +27 -5
  48. synpath-0.2.0/tests/test_opinion.py +430 -0
  49. synpath-0.2.0/tests/test_opinion_live.py +103 -0
  50. synpath-0.2.0/tests/test_opinion_trading.py +414 -0
  51. synpath-0.2.0/tests/test_opinion_ws.py +324 -0
  52. synpath-0.2.0/tests/test_opinion_ws_live.py +92 -0
  53. {synpath-0.1.0 → synpath-0.2.0}/tests/test_routed_limit.py +41 -0
  54. {synpath-0.1.0 → synpath-0.2.0}/tests/test_router.py +62 -0
  55. {synpath-0.1.0 → synpath-0.2.0}/.github/workflows/publish.yml +0 -0
  56. {synpath-0.1.0 → synpath-0.2.0}/.github/workflows/tests.yml +0 -0
  57. {synpath-0.1.0 → synpath-0.2.0}/.gitignore +0 -0
  58. {synpath-0.1.0 → synpath-0.2.0}/LICENSE +0 -0
  59. {synpath-0.1.0 → synpath-0.2.0}/assets/kalshi.png +0 -0
  60. {synpath-0.1.0 → synpath-0.2.0}/assets/polymarket.png +0 -0
  61. {synpath-0.1.0 → synpath-0.2.0}/assets/synpath-banner.png +0 -0
  62. {synpath-0.1.0 → synpath-0.2.0}/clients/typescript/.gitignore +0 -0
  63. {synpath-0.1.0 → synpath-0.2.0}/clients/typescript/example.ts +0 -0
  64. {synpath-0.1.0 → synpath-0.2.0}/clients/typescript/package.json +0 -0
  65. {synpath-0.1.0 → synpath-0.2.0}/clients/typescript/tsconfig.json +0 -0
  66. {synpath-0.1.0 → synpath-0.2.0}/docs/server.md +0 -0
  67. {synpath-0.1.0 → synpath-0.2.0}/examples/track_historical_book.py +0 -0
  68. {synpath-0.1.0 → synpath-0.2.0}/synpath/__main__.py +0 -0
  69. {synpath-0.1.0 → synpath-0.2.0}/synpath/base.py +0 -0
  70. {synpath-0.1.0 → synpath-0.2.0}/synpath/bucket.py +0 -0
  71. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/__init__.py +0 -0
  72. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/alerts.py +0 -0
  73. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/engine.py +0 -0
  74. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/eod.py +0 -0
  75. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/events.py +0 -0
  76. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/fair_values.py +0 -0
  77. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/journal.py +0 -0
  78. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/ledger.py +0 -0
  79. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/__init__.py +0 -0
  80. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/base.py +0 -0
  81. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/day.py +0 -0
  82. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/iceberg.py +0 -0
  83. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/manager.py +0 -0
  84. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/oco.py +0 -0
  85. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/peg.py +0 -0
  86. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/stop.py +0 -0
  87. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/taker.py +0 -0
  88. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/orders/twap.py +0 -0
  89. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/paper.py +0 -0
  90. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/reconcile.py +0 -0
  91. {synpath-0.1.0 → synpath-0.2.0}/synpath/engine/risk.py +0 -0
  92. {synpath-0.1.0 → synpath-0.2.0}/synpath/errors.py +0 -0
  93. {synpath-0.1.0 → synpath-0.2.0}/synpath/history.py +0 -0
  94. {synpath-0.1.0 → synpath-0.2.0}/synpath/hosted.py +0 -0
  95. {synpath-0.1.0 → synpath-0.2.0}/synpath/hosted_auth.py +0 -0
  96. {synpath-0.1.0 → synpath-0.2.0}/synpath/kalshi.py +0 -0
  97. {synpath-0.1.0 → synpath-0.2.0}/synpath/polymarket.py +0 -0
  98. {synpath-0.1.0 → synpath-0.2.0}/synpath/polymarket_us.py +0 -0
  99. {synpath-0.1.0 → synpath-0.2.0}/synpath/remote.py +0 -0
  100. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/__init__.py +0 -0
  101. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/__main__.py +0 -0
  102. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/api.py +0 -0
  103. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/errors.py +0 -0
  104. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/local.py +0 -0
  105. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/models.py +0 -0
  106. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/store.py +0 -0
  107. {synpath-0.1.0 → synpath-0.2.0}/synpath/server/trading.py +0 -0
  108. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/base.py +0 -0
  109. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/errors.py +0 -0
  110. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/kalshi.py +0 -0
  111. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/limiter.py +0 -0
  112. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/money.py +0 -0
  113. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/polymarket.py +0 -0
  114. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/polymarket_signing.py +0 -0
  115. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/polymarket_us.py +0 -0
  116. {synpath-0.1.0 → synpath-0.2.0}/synpath/trading/polymarket_us_exchange.py +0 -0
  117. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/base.py +0 -0
  118. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/grpc.py +0 -0
  119. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/kalshi.py +0 -0
  120. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/polymarket.py +0 -0
  121. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/polymarket_us.py +0 -0
  122. {synpath-0.1.0 → synpath-0.2.0}/synpath/ws/polymarket_us_exchange.py +0 -0
  123. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_candles.json +0 -0
  124. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_events.json +0 -0
  125. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_orderbook.json +0 -0
  126. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_series.json +0 -0
  127. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trades.json +0 -0
  128. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_delete_events_orders.json +0 -0
  129. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_delete_events_orders_batched.json +0 -0
  130. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_delete_events_orders_id.json +0 -0
  131. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_delete_order_groups_id.json +0 -0
  132. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_account_endpoint_costs.json +0 -0
  133. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_account_limits.json +0 -0
  134. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_balance.json +0 -0
  135. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_fills.json +0 -0
  136. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_markets.json +0 -0
  137. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_order_groups_id.json +0 -0
  138. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_orders.json +0 -0
  139. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_orders_id.json +0 -0
  140. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_orders_id_queue_position.json +0 -0
  141. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_positions.json +0 -0
  142. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_series_id.json +0 -0
  143. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_get_settlements.json +0 -0
  144. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_post_events_orders.json +0 -0
  145. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_post_events_orders_batched.json +0 -0
  146. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_post_events_orders_id_amend.json +0 -0
  147. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_post_events_orders_id_decrease.json +0 -0
  148. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_post_order_groups_create.json +0 -0
  149. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_put_order_groups_id_reset.json +0 -0
  150. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/kalshi_trading_put_order_groups_id_trigger.json +0 -0
  151. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_book.json +0 -0
  152. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_events.json +0 -0
  153. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_market.json +0 -0
  154. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_prices.json +0 -0
  155. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_search.json +0 -0
  156. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/poly_trades.json +0 -0
  157. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polymarket_signing_vectors.json +0 -0
  158. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_bbo.json +0 -0
  159. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_book.json +0 -0
  160. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_closed_market.json +0 -0
  161. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_events.json +0 -0
  162. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_market.json +0 -0
  163. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_prices.json +0 -0
  164. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_search.json +0 -0
  165. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_series.json +0 -0
  166. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/polyus_settlement.json +0 -0
  167. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/ws/kalshi_demo_transcript.jsonl +0 -0
  168. {synpath-0.1.0 → synpath-0.2.0}/tests/samples/ws/polymarket_market_transcript.jsonl +0 -0
  169. {synpath-0.1.0 → synpath-0.2.0}/tests/test_client.py +0 -0
  170. {synpath-0.1.0 → synpath-0.2.0}/tests/test_engine.py +0 -0
  171. {synpath-0.1.0 → synpath-0.2.0}/tests/test_engine_demo.py +0 -0
  172. {synpath-0.1.0 → synpath-0.2.0}/tests/test_engine_orders.py +0 -0
  173. {synpath-0.1.0 → synpath-0.2.0}/tests/test_engine_orders_demo.py +0 -0
  174. {synpath-0.1.0 → synpath-0.2.0}/tests/test_feeds.py +0 -0
  175. {synpath-0.1.0 → synpath-0.2.0}/tests/test_history_tracker.py +0 -0
  176. {synpath-0.1.0 → synpath-0.2.0}/tests/test_history_types.py +0 -0
  177. {synpath-0.1.0 → synpath-0.2.0}/tests/test_hosted_auth.py +0 -0
  178. {synpath-0.1.0 → synpath-0.2.0}/tests/test_journal_v3.py +0 -0
  179. {synpath-0.1.0 → synpath-0.2.0}/tests/test_kalshi.py +0 -0
  180. {synpath-0.1.0 → synpath-0.2.0}/tests/test_kalshi_demo.py +0 -0
  181. {synpath-0.1.0 → synpath-0.2.0}/tests/test_kalshi_trading.py +0 -0
  182. {synpath-0.1.0 → synpath-0.2.0}/tests/test_live.py +0 -0
  183. {synpath-0.1.0 → synpath-0.2.0}/tests/test_polymarket.py +0 -0
  184. {synpath-0.1.0 → synpath-0.2.0}/tests/test_polymarket_trading.py +0 -0
  185. {synpath-0.1.0 → synpath-0.2.0}/tests/test_polymarket_us.py +0 -0
  186. {synpath-0.1.0 → synpath-0.2.0}/tests/test_polymarket_us_trading.py +0 -0
  187. {synpath-0.1.0 → synpath-0.2.0}/tests/test_remote_client.py +0 -0
  188. {synpath-0.1.0 → synpath-0.2.0}/tests/test_risk_duplicates.py +0 -0
  189. {synpath-0.1.0 → synpath-0.2.0}/tests/test_routed_recovery.py +0 -0
  190. {synpath-0.1.0 → synpath-0.2.0}/tests/test_serve.py +0 -0
  191. {synpath-0.1.0 → synpath-0.2.0}/tests/test_server.py +0 -0
  192. {synpath-0.1.0 → synpath-0.2.0}/tests/test_server_trading.py +0 -0
  193. {synpath-0.1.0 → synpath-0.2.0}/tests/test_trading_foundations.py +0 -0
  194. {synpath-0.1.0 → synpath-0.2.0}/tests/test_venue_errors.py +0 -0
  195. {synpath-0.1.0 → synpath-0.2.0}/tests/test_ws.py +0 -0
  196. {synpath-0.1.0 → synpath-0.2.0}/tests/test_ws_grpc.py +0 -0
  197. {synpath-0.1.0 → synpath-0.2.0}/tests/test_ws_live.py +0 -0
@@ -37,3 +37,11 @@ POLYMARKET_US_PRIVATE_KEY_PATH=
37
37
  POLYMARKET_US_PARTICIPANT_ID=
38
38
  POLYMARKET_US_ACCOUNT=
39
39
  POLYMARKET_US_ENV=preprod
40
+
41
+ # Opinion (opinion.trade, BNB Chain): the private key of the wallet connected
42
+ # on opinion.trade, and its Open API key (synpath init creates one by signing
43
+ # with that wallet). The key alone also serves the WebSocket and raises the
44
+ # market data rate limit. The account's Safe is read from the venue if unset.
45
+ OPINION_PRIVATE_KEY=
46
+ OPINION_API_KEY=
47
+ OPINION_MULTISIG_ADDRESS=
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: synpath
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: One API for prediction markets. Kalshi and Polymarket, normalized, with every price labelled by source and time.
5
5
  Project-URL: Homepage, https://synpath.dev
6
6
  Project-URL: Documentation, https://synpath.dev/docs
7
7
  Project-URL: Repository, https://github.com/Synpath-ai/synpath
8
8
  Project-URL: Issues, https://github.com/Synpath-ai/synpath/issues
9
- Project-URL: Discord, https://discord.gg/QZzsHTpGA3
9
+ Project-URL: Discord, https://synpath.dev/discord
10
10
  Author: Synpath
11
11
  License: MIT
12
12
  License-File: LICENSE
@@ -52,19 +52,19 @@ Description-Content-Type: text/markdown
52
52
 
53
53
  <img src="https://raw.githubusercontent.com/Synpath-ai/synpath/main/assets/synpath-banner.png" alt="Synpath: one API for every prediction market" width="100%">
54
54
 
55
- # Synpath – One API for prediction markets
55
+ # Synpath – One API for prediction markets <a href="https://x.com/Synpath_Dev"><img src="https://img.shields.io/twitter/url?url=https%3A%2F%2Fx.com%2FSynpath_Dev&style=social&label=Follow" alt="Follow @Synpath_Dev on X" height="28"></a>
56
56
 
57
57
  **Build on prediction markets without building against each one.**<br/>
58
- One open-source API across Kalshi, Polymarket and Polymarket US: market data, order entry, live streams, and an execution engine that remembers what it sent. In-process from Python, over REST and WebSocket from anywhere.
58
+ One open-source API across Kalshi, Polymarket, Polymarket US and Opinion: market data, order entry, live streams, and an execution engine that remembers what it sent. In-process from Python, over REST and WebSocket from anywhere.
59
59
 
60
60
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
61
- <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
62
- <a href="https://discord.gg/QZzsHTpGA3"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
61
+ <a href="https://pepy.tech/projects/synpath"><img src="https://img.shields.io/pepy/dt/synpath?label=Total%20Downloads&color=blue" alt="Total Downloads"></a>
62
+ <a href="https://synpath.dev/discord"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
63
63
 
64
64
  </div>
65
65
 
66
- **🆕 New: Every Kalshi order book, every tick.**<br/>
67
- Full-depth books and trades for every past Kalshi trade. Query any moment in under a minute. **[Get the data →](https://synpath.dev/tick-data)**
66
+ **🆕 New: Smart order routing across Kalshi and Polymarket.**<br/>
67
+ One order, both order books combined, filled from the cheapest price after fees. **[See how it works →](https://www.synpath.dev/docs/concepts/buckets)**
68
68
 
69
69
  ---
70
70
 
@@ -75,10 +75,11 @@ Full-depth books and trades for every past Kalshi trade. Query any moment in und
75
75
  | <img src="assets/kalshi.png" width="20" height="20" alt="Kalshi"> | [Kalshi](https://kalshi.com) | `kalshi` | ✓ | ✓ | WebSocket | one book per market, both sides read it |
76
76
  | <img src="assets/polymarket.png" width="20" height="20" alt="Polymarket"> | [Polymarket](https://polymarket.com) | `polymarket` | ✓ | ✓ | WebSocket | one book per outcome token |
77
77
  | <img src="assets/polymarket.png" width="20" height="20" alt="Polymarket US"> | [Polymarket US](https://polymarket.us) | `polymarket_us` | ✓ | ✓ retail and exchange APIs | WebSocket and gRPC | one book per market, both sides read it |
78
+ | | [Opinion](https://opinion.trade) | `opinion` | ✓ | ✓ not yet live-tested | WebSocket (API key) | one book per outcome token |
78
79
 
79
80
  ## Why Synpath
80
81
 
81
- - **Liquidity is fragmented. Your time shouldn't be.** The same market trades on Kalshi, Polymarket and Polymarket US, each with its own API, units and quirks. Synpath gives you one interface for all of them, and smart order routing buys from whichever book is cheapest. Spend your time on alpha, not plumbing.
82
+ - **Liquidity is fragmented. Your time shouldn't be.** The same market trades on Kalshi, Polymarket, Polymarket US and Opinion, each with its own API, units and quirks. Synpath gives you one interface for all of them, and smart order routing buys from whichever book is cheapest. Spend your time on alpha, not plumbing.
82
83
 
83
84
  - **Traders deserve advanced order types.** Stops, trailing stops, icebergs, OCO, brackets, TWAP and pegs, on every venue, even where the exchange has none. Orders are journaled before they're sent, so a crash never places one twice.
84
85
 
@@ -124,7 +125,7 @@ fee.estimate(price=0.50, contracts=100) # 1.75
124
125
  **Same code, every venue**
125
126
 
126
127
  ```python
127
- for venue_id in synpath.exchanges: # ['kalshi', 'polymarket', 'polymarket_us']
128
+ for venue_id in synpath.exchanges: # ['kalshi', 'polymarket', 'polymarket_us', 'opinion']
128
129
  with synpath.exchange(venue_id) as venue:
129
130
  page = venue.fetch_markets(limit=5)
130
131
 
@@ -157,18 +158,26 @@ async with PolymarketMarketStream() as stream:
157
158
  print(event.market_id, event.side, event.best_bid, event.best_ask)
158
159
  ```
159
160
 
160
- **The engine, and the orders the venues do not hold**
161
+ **Smart order routing: one order across Kalshi and Polymarket**
161
162
 
162
163
  ```python
163
- from synpath.engine import Engine, EngineConfig, RiskConfig
164
- from synpath import OrderType
165
-
166
- async with Engine({"kalshi": kalshi}, EngineConfig(journal_path="trading.db"),
167
- risk=RiskConfig(max_order_contracts=Decimal("100"))) as engine:
164
+ from synpath import Bucket, BucketMember, OrderType, PolymarketTrading
165
+ from synpath.engine import Engine, EngineConfig
166
+
167
+ creds = load_credentials()
168
+ async with KalshiTrading(require("kalshi", creds)) as kalshi, \
169
+ PolymarketTrading(require("polymarket", creds)) as poly, \
170
+ Engine({"kalshi": kalshi, "polymarket": poly}, EngineConfig(journal_path="trading.db")) as engine:
171
+ # The same market on both venues; flip=True where a venue asks the question the other way round
172
+ bucket = await engine.save_bucket(Bucket(book="alpha", name="Fed cut in December", members=[
173
+ BucketMember(market_id="kalshi:KXFEDDECISION-26DEC-C25"),
174
+ BucketMember(market_id="polymarket:2252244", flip=True),
175
+ ]))
176
+ # Both order books combined, each level net of fees, filled from the cheapest price outward
168
177
  await engine.submit(OrderRequest(
169
- market_id=market.id, side=Side.SELL, amount=Decimal("20"),
170
- type=OrderType.TRAILING_STOP, stop_price=Decimal("0.40"),
171
- params={"trail": "0.03"}, book="alpha",
178
+ market_id=bucket.market_id, side=Side.BUY, amount=Decimal("500"),
179
+ type=OrderType.MARKET, price=Decimal("0.45"), # the worst price you accept
180
+ book="alpha",
172
181
  ))
173
182
  ```
174
183
 
@@ -189,14 +198,7 @@ npx openapi-typescript openapi.json --default-non-nullable false -o src/synpath.
189
198
 
190
199
  ## Documentation
191
200
 
192
- - [CLI](https://synpath.dev/docs/quickstart): hosted data API sign-in and keys versus local venue trading server setup
193
- - [Unified API](https://synpath.dev/docs/sdks/python): methods, data structures, identifiers, paging, status, errors
194
- - [Capabilities](https://synpath.dev/docs/concepts/exchanges): what each venue can and cannot answer
195
- - [HTTP server](https://synpath.dev/docs/api-reference): routes, envelopes, typed client generation, trading routes with per-account keys, mounting behind your own auth
196
- - [Trading](https://synpath.dev/docs/concepts/orders): credentials, `Decimal` money, order entry on Kalshi, Polymarket and Polymarket US
197
- - [Streaming](https://synpath.dev/docs/ws-api-reference): WebSocket and gRPC books, trades, orders and fills as self-healing event streams
198
- - [Engine](docs/engine.md): a journal written before anything is sent, pre-trade risk, a ledger, reconciliation, a kill switch, the order types the venues do not hold, buckets that route one order across venues, paper trading
199
- - [TypeScript client](https://synpath.dev/docs/sdks/typescript): generate a typed client from the OpenAPI document, place through cancel without Python
201
+ See the [API Reference](https://www.synpath.dev/docs) for detailed documentation and more examples.
200
202
 
201
203
  ## Development
202
204
 
@@ -217,7 +219,7 @@ secret.
217
219
 
218
220
  ## Prior Art
219
221
 
220
- [ccxt](https://github.com/ccxt/ccxt) set the conventions this library follows. [pmxt](https://github.com/pmxt-dev/pmxt) got to prediction markets first.
222
+ [ccxt](https://github.com/ccxt/ccxt) set the conventions this library follows.
221
223
 
222
224
  ## License
223
225
 
@@ -2,19 +2,19 @@
2
2
 
3
3
  <img src="https://raw.githubusercontent.com/Synpath-ai/synpath/main/assets/synpath-banner.png" alt="Synpath: one API for every prediction market" width="100%">
4
4
 
5
- # Synpath – One API for prediction markets
5
+ # Synpath – One API for prediction markets <a href="https://x.com/Synpath_Dev"><img src="https://img.shields.io/twitter/url?url=https%3A%2F%2Fx.com%2FSynpath_Dev&style=social&label=Follow" alt="Follow @Synpath_Dev on X" height="28"></a>
6
6
 
7
7
  **Build on prediction markets without building against each one.**<br/>
8
- One open-source API across Kalshi, Polymarket and Polymarket US: market data, order entry, live streams, and an execution engine that remembers what it sent. In-process from Python, over REST and WebSocket from anywhere.
8
+ One open-source API across Kalshi, Polymarket, Polymarket US and Opinion: market data, order entry, live streams, and an execution engine that remembers what it sent. In-process from Python, over REST and WebSocket from anywhere.
9
9
 
10
10
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
11
- <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
12
- <a href="https://discord.gg/QZzsHTpGA3"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
11
+ <a href="https://pepy.tech/projects/synpath"><img src="https://img.shields.io/pepy/dt/synpath?label=Total%20Downloads&color=blue" alt="Total Downloads"></a>
12
+ <a href="https://synpath.dev/discord"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
13
13
 
14
14
  </div>
15
15
 
16
- **🆕 New: Every Kalshi order book, every tick.**<br/>
17
- Full-depth books and trades for every past Kalshi trade. Query any moment in under a minute. **[Get the data →](https://synpath.dev/tick-data)**
16
+ **🆕 New: Smart order routing across Kalshi and Polymarket.**<br/>
17
+ One order, both order books combined, filled from the cheapest price after fees. **[See how it works →](https://www.synpath.dev/docs/concepts/buckets)**
18
18
 
19
19
  ---
20
20
 
@@ -25,10 +25,11 @@ Full-depth books and trades for every past Kalshi trade. Query any moment in und
25
25
  | <img src="assets/kalshi.png" width="20" height="20" alt="Kalshi"> | [Kalshi](https://kalshi.com) | `kalshi` | ✓ | ✓ | WebSocket | one book per market, both sides read it |
26
26
  | <img src="assets/polymarket.png" width="20" height="20" alt="Polymarket"> | [Polymarket](https://polymarket.com) | `polymarket` | ✓ | ✓ | WebSocket | one book per outcome token |
27
27
  | <img src="assets/polymarket.png" width="20" height="20" alt="Polymarket US"> | [Polymarket US](https://polymarket.us) | `polymarket_us` | ✓ | ✓ retail and exchange APIs | WebSocket and gRPC | one book per market, both sides read it |
28
+ | | [Opinion](https://opinion.trade) | `opinion` | ✓ | ✓ not yet live-tested | WebSocket (API key) | one book per outcome token |
28
29
 
29
30
  ## Why Synpath
30
31
 
31
- - **Liquidity is fragmented. Your time shouldn't be.** The same market trades on Kalshi, Polymarket and Polymarket US, each with its own API, units and quirks. Synpath gives you one interface for all of them, and smart order routing buys from whichever book is cheapest. Spend your time on alpha, not plumbing.
32
+ - **Liquidity is fragmented. Your time shouldn't be.** The same market trades on Kalshi, Polymarket, Polymarket US and Opinion, each with its own API, units and quirks. Synpath gives you one interface for all of them, and smart order routing buys from whichever book is cheapest. Spend your time on alpha, not plumbing.
32
33
 
33
34
  - **Traders deserve advanced order types.** Stops, trailing stops, icebergs, OCO, brackets, TWAP and pegs, on every venue, even where the exchange has none. Orders are journaled before they're sent, so a crash never places one twice.
34
35
 
@@ -74,7 +75,7 @@ fee.estimate(price=0.50, contracts=100) # 1.75
74
75
  **Same code, every venue**
75
76
 
76
77
  ```python
77
- for venue_id in synpath.exchanges: # ['kalshi', 'polymarket', 'polymarket_us']
78
+ for venue_id in synpath.exchanges: # ['kalshi', 'polymarket', 'polymarket_us', 'opinion']
78
79
  with synpath.exchange(venue_id) as venue:
79
80
  page = venue.fetch_markets(limit=5)
80
81
 
@@ -107,18 +108,26 @@ async with PolymarketMarketStream() as stream:
107
108
  print(event.market_id, event.side, event.best_bid, event.best_ask)
108
109
  ```
109
110
 
110
- **The engine, and the orders the venues do not hold**
111
+ **Smart order routing: one order across Kalshi and Polymarket**
111
112
 
112
113
  ```python
113
- from synpath.engine import Engine, EngineConfig, RiskConfig
114
- from synpath import OrderType
115
-
116
- async with Engine({"kalshi": kalshi}, EngineConfig(journal_path="trading.db"),
117
- risk=RiskConfig(max_order_contracts=Decimal("100"))) as engine:
114
+ from synpath import Bucket, BucketMember, OrderType, PolymarketTrading
115
+ from synpath.engine import Engine, EngineConfig
116
+
117
+ creds = load_credentials()
118
+ async with KalshiTrading(require("kalshi", creds)) as kalshi, \
119
+ PolymarketTrading(require("polymarket", creds)) as poly, \
120
+ Engine({"kalshi": kalshi, "polymarket": poly}, EngineConfig(journal_path="trading.db")) as engine:
121
+ # The same market on both venues; flip=True where a venue asks the question the other way round
122
+ bucket = await engine.save_bucket(Bucket(book="alpha", name="Fed cut in December", members=[
123
+ BucketMember(market_id="kalshi:KXFEDDECISION-26DEC-C25"),
124
+ BucketMember(market_id="polymarket:2252244", flip=True),
125
+ ]))
126
+ # Both order books combined, each level net of fees, filled from the cheapest price outward
118
127
  await engine.submit(OrderRequest(
119
- market_id=market.id, side=Side.SELL, amount=Decimal("20"),
120
- type=OrderType.TRAILING_STOP, stop_price=Decimal("0.40"),
121
- params={"trail": "0.03"}, book="alpha",
128
+ market_id=bucket.market_id, side=Side.BUY, amount=Decimal("500"),
129
+ type=OrderType.MARKET, price=Decimal("0.45"), # the worst price you accept
130
+ book="alpha",
122
131
  ))
123
132
  ```
124
133
 
@@ -139,14 +148,7 @@ npx openapi-typescript openapi.json --default-non-nullable false -o src/synpath.
139
148
 
140
149
  ## Documentation
141
150
 
142
- - [CLI](https://synpath.dev/docs/quickstart): hosted data API sign-in and keys versus local venue trading server setup
143
- - [Unified API](https://synpath.dev/docs/sdks/python): methods, data structures, identifiers, paging, status, errors
144
- - [Capabilities](https://synpath.dev/docs/concepts/exchanges): what each venue can and cannot answer
145
- - [HTTP server](https://synpath.dev/docs/api-reference): routes, envelopes, typed client generation, trading routes with per-account keys, mounting behind your own auth
146
- - [Trading](https://synpath.dev/docs/concepts/orders): credentials, `Decimal` money, order entry on Kalshi, Polymarket and Polymarket US
147
- - [Streaming](https://synpath.dev/docs/ws-api-reference): WebSocket and gRPC books, trades, orders and fills as self-healing event streams
148
- - [Engine](docs/engine.md): a journal written before anything is sent, pre-trade risk, a ledger, reconciliation, a kill switch, the order types the venues do not hold, buckets that route one order across venues, paper trading
149
- - [TypeScript client](https://synpath.dev/docs/sdks/typescript): generate a typed client from the OpenAPI document, place through cancel without Python
151
+ See the [API Reference](https://www.synpath.dev/docs) for detailed documentation and more examples.
150
152
 
151
153
  ## Development
152
154
 
@@ -167,7 +169,7 @@ secret.
167
169
 
168
170
  ## Prior Art
169
171
 
170
- [ccxt](https://github.com/ccxt/ccxt) set the conventions this library follows. [pmxt](https://github.com/pmxt-dev/pmxt) got to prediction markets first.
172
+ [ccxt](https://github.com/ccxt/ccxt) set the conventions this library follows.
171
173
 
172
174
  ## License
173
175
 
@@ -68,7 +68,7 @@ and spot exchanges do not.
68
68
  | Method | Returns | Notes |
69
69
  |---|---|---|
70
70
  | `fetch_markets(query, limit, cursor, status, sort)` | `Page[Market]` | `limit` up to 100 |
71
- | `fetch_markets_by_ids(ids)` | `list[Market]` | batched (Kalshi 200, Polymarket 100, Polymarket US 50 ids a request), order preserved, closed and settled markets included |
71
+ | `fetch_markets_by_ids(ids)` | `list[Market]` | batched (Kalshi 200, Polymarket 100, Polymarket US 50 ids a request; Opinion has no batch lookup, one request each), order preserved, closed and settled markets included |
72
72
  | `fetch_events(query, limit, cursor, status)` | `Page[Event]` | markets nested |
73
73
  | `fetch_market(market_id)` | `Market` | |
74
74
  | `fetch_order_book(market_id, side, depth)` | `OrderBook` | `side="yes"` (default) or `"no"` |
@@ -6,21 +6,21 @@ Read `has` before you call. A capability a venue lacks raises `NotSupported`
6
6
  rather than returning an empty list, so "cannot" is never mistaken for "there
7
7
  is nothing".
8
8
 
9
- | Method | `kalshi` | `polymarket` | `polymarket_us` |
10
- |---|---|---|---|
11
- | `fetch_markets` | yes | yes | yes |
12
- | `fetch_events` | yes | yes | yes |
13
- | `fetch_market` | yes | yes | yes |
14
- | `fetch_order_book` | yes | yes | yes |
15
- | `fetch_order_books` (batched) | yes, one request per market | yes, one round trip | yes, one request per market |
16
- | `fetch_trades` | yes | yes | no public tape over REST; the last trade is on the book |
17
- | `fetch_ohlcv` | yes, the venue's candles | yes, built from the trade tape, both tokens folded into the YES price; the venue's quote samples via `source="quotes"` | partial: quote-derived bid/ask midpoints, no volume; no public tape to build from |
18
- | `fetch_series` | yes | no series tier | yes, but fees are per market, not per series |
19
- | `fetch_fee_schedule` | yes | yes | yes |
20
- | `search` | yes ([undocumented host](api.md#search)) | yes | yes |
21
- | `fetch_markets_by_ids` | yes | yes | yes |
22
- | `sort` | yes, the page is ordered after it is read | yes, at the venue | yes, the page is ordered after it is read; `volume` and `liquidity` cost one read per market |
23
- | `match_market` / `match_event` | no | no | no |
9
+ | Method | `kalshi` | `polymarket` | `polymarket_us` | `opinion` |
10
+ |---|---|---|---|---|
11
+ | `fetch_markets` | yes | yes | yes | yes, categorical topics flattened into their options |
12
+ | `fetch_events` | yes | yes | yes | yes, one per topic |
13
+ | `fetch_market` | yes | yes | yes | yes; a categorical option costs a second read for its topic |
14
+ | `fetch_order_book` | yes | yes | yes | yes |
15
+ | `fetch_order_books` (batched) | yes, one request per market | yes, one round trip | yes, one request per market | yes, one request per market |
16
+ | `fetch_trades` | yes | yes | no public tape over REST; the last trade is on the book | no public tape; the last trade is on `refresh_quotes` |
17
+ | `fetch_ohlcv` | yes, the venue's candles | yes, built from the trade tape, both tokens folded into the YES price; the venue's quote samples via `source="quotes"` | partial: quote-derived bid/ask midpoints, no volume; no public tape to build from | partial: last-trade samples, hourly or daily, no volume |
18
+ | `fetch_series` | yes | no series tier | yes, but fees are per market, not per series | no series tier |
19
+ | `fetch_fee_schedule` | yes | yes | yes | yes, read from the venue's fee contract on BNB Chain |
20
+ | `search` | yes ([undocumented host](api.md#search)) | yes | yes | partial: no venue search; titles matched here over 25 catalog pages |
21
+ | `fetch_markets_by_ids` | yes | yes | yes | yes, one request per market |
22
+ | `sort` | yes, the page is ordered after it is read | yes, at the venue | yes, the page is ordered after it is read; `volume` and `liquidity` cost one read per market | partial: `volume` and `newest` at the venue; no liquidity figure exists |
23
+ | `match_market` / `match_event` | no | no | no | no |
24
24
 
25
25
  Polymarket US is the CFTC-regulated exchange, not the on-chain CLOB the
26
26
  `polymarket` adapter reads; the two share a brand and nothing else. It has
@@ -29,6 +29,13 @@ marked `derived=True` as on Kalshi. Its catalog payloads carry no volume, no
29
29
  sizes and no last trade; `refresh_quotes(market)` reads the book once for all
30
30
  of them, and for the market's live state.
31
31
 
32
+ Opinion lists *topics*: a binary topic is one market, a categorical topic
33
+ holds one binary market per option, and only those trade. Each topic is an
34
+ event here. Its catalog carries no prices at all, so `refresh_quotes(market)`
35
+ reads both books and the last trade. Statuses `open`, `settled` and `all`
36
+ filter at the venue; `closed` cannot and raises. An API key is optional and
37
+ only raises the rate limit (`Opinion(api_key=...)`).
38
+
32
39
  Matching spans venues, so no adapter claims it. `synpath.match_market(id)`
33
40
  and `synpath.match_event(id)` are answered by Synpath's hosted matching service
34
41
  at `api.synpath.dev`, with a Synpath API key (the one `synpath keys create` saved, `SYNPATH_API_KEY`, or `api_key=`). See
@@ -56,20 +63,20 @@ install. Polymarket US has two, because the venue has
56
63
  two: a retail API any verified account uses, and an exchange API for
57
64
  onboarded firms.
58
65
 
59
- | Method | `kalshi` | `polymarket` | `polymarket_us` (retail) | `polymarket_us` (exchange) |
60
- |---|---|---|---|---|
61
- | `create_order`, `create_orders` | yes | yes | yes | yes |
62
- | `cancel_order`, `cancel_orders`, `cancel_all_orders` | yes | yes | yes | yes |
63
- | `edit_order` | yes, amend or decrease | no; an edit is a cancel and a new order | yes, price, quantity and time in force | yes |
64
- | `fetch_order`, `fetch_open_orders` | yes | yes | yes | yes |
65
- | `fetch_orders` (by status or time) | yes | no | no | yes, rationed to 12 requests a minute |
66
- | `fetch_my_trades` | yes | yes | no; the activity feed carries no order id | yes |
67
- | `fetch_positions`, `fetch_balance` | yes | yes | yes | yes |
68
- | `fetch_settlements` | yes | yes, from the redemption activity | yes | no |
69
- | `fetch_queue_position` | yes | no | no | no |
70
- | `fetch_fee_estimate` | yes | yes | yes | yes |
71
- | `rfq` | yes | no | no | no |
72
- | `split_merge` | no; contracts are not tokens | partial: Deposit Wallets and EOAs | no | no |
66
+ | Method | `kalshi` | `polymarket` | `polymarket_us` (retail) | `polymarket_us` (exchange) | `opinion` (not yet live-tested) |
67
+ |---|---|---|---|---|---|
68
+ | `create_order`, `create_orders` | yes | yes | yes | yes | one at a time; `market` and `ioc` are limits with the rest cancelled |
69
+ | `cancel_order`, `cancel_orders`, `cancel_all_orders` | yes | yes | yes | yes | one at a time; `cancel_all_orders` lists then cancels |
70
+ | `edit_order` | yes, amend or decrease | no; an edit is a cancel and a new order | yes, price, quantity and time in force | yes | no |
71
+ | `fetch_order`, `fetch_open_orders` | yes | yes | yes | yes | yes |
72
+ | `fetch_orders` (by status or time) | yes | no | no | yes, rationed to 12 requests a minute | yes, by status |
73
+ | `fetch_my_trades` | yes | yes | no; the activity feed carries no order id | yes | yes, confirmed fills |
74
+ | `fetch_positions`, `fetch_balance` | yes | yes | yes | yes | yes |
75
+ | `fetch_settlements` | yes | yes, from the redemption activity | yes | no | no |
76
+ | `fetch_queue_position` | yes | no | no | no | no |
77
+ | `fetch_fee_estimate` | yes | yes | yes | yes | yes, from the fee contract, 0.25 USDT minimum |
78
+ | `rfq` | yes | no | no | no | no |
79
+ | `split_merge` | no; contracts are not tokens | partial: Deposit Wallets and EOAs | no | no | no |
73
80
 
74
81
  Order types are a separate question. A venue holds `limit` and `market`;
75
82
  everything else -- stops, icebergs, brackets, TWAP, pegs -- is held by the
@@ -93,6 +100,8 @@ Every stream answers the `watch_*` keys the same way
93
100
  | `PolymarketUSPrivateStream` | no | no | no | no | yes | yes | yes | yes |
94
101
  | `PolymarketUSExchangeMarketDataStream` | yes | yes | no | no | no | no | no | no |
95
102
  | `PolymarketUSExchangeOrderStream` | no | no | no | no | yes | yes | no | no |
103
+ | `OpinionMarketStream` | yes | partial | yes | no | no | no | no | no |
104
+ | `OpinionUserStream` | no | no | no | no | yes | yes | no | no |
96
105
 
97
106
  The exchange API's other gRPC streams carry one thing each and say so:
98
107
  drop copy and trade capture carry fills, position and position-change carry
@@ -159,8 +159,12 @@ bound would walk each book to its end; a limit order on a bucket is refused. The
159
159
  lays the members' books side by side in bucket terms, a flipped member's
160
160
  YES book read as its NO book, prices every level net of the taker fee, and
161
161
  walks from the best net price to that worst price. A leg under its venue's minimum
162
- is dropped and its size moves to the next venue. The legs never sum to
163
- more than was asked.
162
+ -- in contracts, or in value where the venue counts value (Opinion: 5 USDT
163
+ of the token bought) -- is dropped and its size moves to the next venue. A
164
+ venue that charges a minimum fee per order (Opinion: 0.25 USDT) has the
165
+ floor added to each leg's cost: a leg it pushes past the worst price is
166
+ dropped, and one whose size fills as fully for less elsewhere is moved. The
167
+ legs never sum to more than was asked.
164
168
 
165
169
  The parent re-plans whenever a leg fills, a book moves or a leg is pulled.
166
170
  A leg still at a price the plan wants is kept, so it holds its place in the
@@ -31,6 +31,8 @@ asyncio.run(main())
31
31
  | `PolymarketUserStream` | CLOB user channel | CLOB API credentials, or a `PolymarketTrading` to derive them | orders; fills with settlement state |
32
32
  | `PolymarketUSMarketStream` | `api.polymarket.us/v1/ws/markets` | retail API key | full books, top of book, trades, market state |
33
33
  | `PolymarketUSPrivateStream` | `api.polymarket.us/v1/ws/private` | retail API key | orders, fills, positions, balances |
34
+ | `OpinionMarketStream` | `ws.opinion.trade` | Opinion API key (every channel needs one) | books, trades, last prices |
35
+ | `OpinionUserStream` | `ws.opinion.trade` | Opinion API key | orders; fills once the chain confirms them |
34
36
 
35
37
  Each stream's `has` answers the `watch_*` capability keys (`watch_order_book`,
36
38
  `watch_ticker`, `watch_trades`, `watch_market_status`, `watch_orders`,
@@ -85,6 +87,15 @@ jittered exponential backoff between attempts.
85
87
  a fresh snapshot.
86
88
  - *Polymarket US* sends the whole visible book every time, so books cannot
87
89
  drift.
90
+ - *Opinion* sends one changed level per message, with no snapshot and no
91
+ sequence number. The stream reads each book over REST once the
92
+ subscription is live and replays the changes that arrived during the read.
93
+ A change is read as the level's whole size (the venue documents `size` as
94
+ the level's shares), so replaying one the snapshot already holds is
95
+ harmless; `tests/test_opinion_ws_live.py` checks this against the venue.
96
+ Nothing in the feed reveals a missed message, so every book is read again
97
+ on a timer (`resync_interval`, 60 s by default): a book that disagrees
98
+ while no change was in flight is a `gap`, and is replaced.
88
99
 
89
100
  **A book is right or marked not ready.** After a gap or a disconnect the
90
101
  book's `ready` is false until a snapshot arrives; deltas are not applied to
@@ -214,6 +225,10 @@ documented heartbeat and rely on gRPC keepalive.
214
225
  covers.
215
226
  - **Polymarket US:** built to the venue's documentation and SDK, with the
216
227
  envelope spellings both of them accept.
228
+ - **Opinion:** built to the venue's documentation and SDK samples, with the
229
+ REST books recorded for the read adapter. `tests/test_opinion_ws_live.py`
230
+ checks streamed books against the venue's own over a live minute; it needs
231
+ `OPINION_API_KEY`.
217
232
  - **Polymarket US exchange API (gRPC):** a real local gRPC server speaking
218
233
  the venue's own compiled messages. It covers a refused token, a snapshot,
219
234
  an execution report and a drop, and a drop-copy resume token surviving a
@@ -53,6 +53,7 @@ they point at and the public half of the identity. It prints no secret.
53
53
  | Polymarket | `POLYMARKET_PRIVATE_KEY`, `POLYMARKET_SIGNATURE_TYPE` (3 Deposit Wallet, used by every account created since May 2026 and what `synpath init` suggests; 1 proxy, 2 Safe, 0 allowlisted EOA, which is also what an unset value means), `POLYMARKET_FUNDER` (the wallet address, required for 1-3); optional `POLYMARKET_API_KEY` / `_API_SECRET` / `_API_PASSPHRASE` (derived when absent), `POLYMARKET_BUILDER_CODE`, `POLYMARKET_RELAYER_API_KEY` / `_RELAYER_API_KEY_ADDRESS` (gasless wallet transactions), `POLYMARKET_RPC_URL` (an EOA's own transactions) |
54
54
  | Polymarket US, retail API | `POLYMARKET_US_KEY_ID`, `POLYMARKET_US_SECRET_KEY` (from polymarket.us/developer) |
55
55
  | Polymarket US, exchange API | `POLYMARKET_US_CLIENT_ID`, `POLYMARKET_US_PRIVATE_KEY_PATH`, `POLYMARKET_US_PARTICIPANT_ID`, `POLYMARKET_US_ACCOUNT` (optional), `POLYMARKET_US_ENV` = `preprod` or `prod` |
56
+ | Opinion | `OPINION_PRIVATE_KEY` (the wallet connected on opinion.trade), `OPINION_API_KEY` (`synpath init` creates it by signing with that wallet), `OPINION_MULTISIG_ADDRESS` (optional; the account's Safe, read from the venue when absent) |
56
57
 
57
58
  ## Through your own server
58
59
 
@@ -316,3 +317,32 @@ five-second latency stopgap, not a rate limit, and maps to `OrderRejected`
316
317
  with `reason="latency_stopgap"`: safe to resend. Venue `day` orders do not
317
318
  cancel at the session roll, so `day` is refused here as on the other venues.
318
319
 
320
+ ## Opinion
321
+
322
+ `OpinionTrading`. **Not yet tested against the live venue**: it is built to
323
+ Opinion's documentation and its own SDK, with signing and amounts checked
324
+ byte for byte against the SDK's code. Start small.
325
+
326
+ Orders are the CTF exchange's EIP-712 `Order` on BNB Chain, signed by the
327
+ wallet for the account's Safe, which holds the USDT and the tokens; the
328
+ Safe's address is read from the venue when not configured. YES and NO are
329
+ separate tokens, as on Polymarket: `buy` buys YES, `sell` buys NO at
330
+ `1 - price`, and with `reduce_only` they sell the tokens held instead.
331
+
332
+ The venue rests limit orders until cancelled and has nothing else: no time
333
+ in force, no post-only, and a market buy that spends a USDT amount rather
334
+ than taking a number of contracts. So `market` and `ioc` are sent as a limit
335
+ at the price given and the unmatched rest is cancelled at once; `fok`, `gtd`
336
+ and `post_only` are refused. The two amounts of an order must state its
337
+ price exactly, which trims the size to four significant digits of its USDT
338
+ value: the order's `amount` is what was actually sent. The minimum order is
339
+ 5 USDT. Fees are taker only, `rate * notional * p * (1 - p)` with the rate
340
+ read from the venue's fee contract, and at least 0.25 USDT an order.
341
+
342
+ A match moves an order's filled figures, and produces a trade, only once the
343
+ chain confirms it. `OpinionUserStream` carries both
344
+ ([Streaming](streaming.md)). The one-time approvals ("enable trading") are
345
+ made on opinion.trade; split, merge and redeem are not built here.
346
+
347
+ Opinion's terms bar residents and citizens of several countries, the UK and
348
+ US among them: check them before trading.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "synpath"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "One API for prediction markets. Kalshi and Polymarket, normalized, with every price labelled by source and time."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -57,7 +57,7 @@ Homepage = "https://synpath.dev"
57
57
  Documentation = "https://synpath.dev/docs"
58
58
  Repository = "https://github.com/Synpath-ai/synpath"
59
59
  Issues = "https://github.com/Synpath-ai/synpath/issues"
60
- Discord = "https://discord.gg/QZzsHTpGA3"
60
+ Discord = "https://synpath.dev/discord"
61
61
 
62
62
  [tool.hatch.build.targets.wheel]
63
63
  packages = ["synpath"]
@@ -42,6 +42,7 @@ from .errors import (
42
42
  from .kalshi import Kalshi
43
43
  from .polymarket import Polymarket
44
44
  from .polymarket_us import PolymarketUS
45
+ from .opinion import Opinion
45
46
  from .matching import match_event, match_market
46
47
  from .history import fetch_order_book_at, fetch_order_book_range, fetch_trades_range
47
48
  from .bucket import Bucket, BucketMember, BucketOrderReport, BucketPosition
@@ -75,12 +76,13 @@ from .types import (
75
76
  TradesRangeResponse,
76
77
  )
77
78
 
78
- __version__ = "0.1.0"
79
+ __version__ = "0.2.0"
79
80
 
80
81
  exchanges: dict[str, type[Exchange]] = {
81
82
  "kalshi": Kalshi,
82
83
  "polymarket": Polymarket,
83
84
  "polymarket_us": PolymarketUS,
85
+ "opinion": Opinion,
84
86
  }
85
87
  """Every venue this library speaks, by id."""
86
88
 
@@ -101,6 +103,7 @@ def exchange(venue: str, **kwargs) -> Exchange:
101
103
  # keeps working. Imported last, after the read API it builds on.
102
104
  from .trading import (
103
105
  TradingExchange, KalshiTrading, PolymarketTrading, PolymarketUSTrading, PolymarketUSExchangeTrading,
106
+ OpinionTrading,
104
107
  Account, Balance, EditRequest, FeeEstimate, Fill, HeldBy, Liquidity,
105
108
  Order, OrderRequest, OrderStatus, OrderType, Position, PositionSide,
106
109
  Precision, Settlement, SettlementState, Side, TimeInForce,
@@ -128,6 +131,7 @@ _STREAM_CLASSES = (
128
131
  "PolymarketUSExchangeTradeCaptureStream", "PolymarketUSExchangePositionChangeStream",
129
132
  "PolymarketUSExchangeInstrumentStream", "PolymarketUSExchangePositionStream",
130
133
  "PolymarketUSExchangeMarketDataStream", "PolymarketUSExchangeBalanceLedgerStream",
134
+ "OpinionMarketStream", "OpinionUserStream",
131
135
  )
132
136
  _SERVER_NAMES = ("create_app", "create_trading_app", "VenueRegistry", "ControlStore", "Principal")
133
137
 
@@ -149,7 +153,7 @@ def __getattr__(name: str):
149
153
 
150
154
 
151
155
  __all__ = [
152
- "Kalshi", "Polymarket", "PolymarketUS", "Exchange", "exchange", "exchanges",
156
+ "Kalshi", "Polymarket", "PolymarketUS", "Opinion", "Exchange", "exchange", "exchanges",
153
157
  "Capability", "HttpClient", "RateLimiter",
154
158
  "Market", "Event", "Outcome", "Quote", "MarketStats", "OrderBook",
155
159
  "OrderLevel", "Trade", "Candle", "FeeSchedule", "Series", "Page",
@@ -166,6 +170,7 @@ __all__ = [
166
170
  "MarketNotFound", "NotSupported", "AuthenticationError",
167
171
  # order entry
168
172
  "TradingExchange", "KalshiTrading", "PolymarketTrading", "PolymarketUSTrading", "PolymarketUSExchangeTrading",
173
+ "OpinionTrading",
169
174
  "Account", "Balance", "EditRequest", "FeeEstimate", "Fill", "HeldBy", "Liquidity",
170
175
  "Order", "OrderRequest", "OrderStatus", "OrderType", "Position", "PositionSide",
171
176
  "Precision", "Settlement", "SettlementState", "Side", "TimeInForce",
@@ -353,4 +353,9 @@ def _polymarket_us(creds: Credentials) -> TradingExchange:
353
353
  return PolymarketUSTrading(creds) # type: ignore[arg-type]
354
354
 
355
355
 
356
- _TRADING = {"kalshi": _kalshi, "polymarket": _polymarket, "polymarket_us": _polymarket_us}
356
+ def _opinion(creds: Credentials) -> TradingExchange:
357
+ from .trading.opinion import OpinionTrading
358
+ return OpinionTrading(creds) # type: ignore[arg-type]
359
+
360
+
361
+ _TRADING = {"kalshi": _kalshi, "polymarket": _polymarket, "polymarket_us": _polymarket_us, "opinion": _opinion}
@@ -60,6 +60,7 @@ ADAPTERS = {
60
60
  "polymarket": ("..trading.polymarket", "PolymarketTrading"),
61
61
  "polymarket_us": ("..trading.polymarket_us", "PolymarketUSTrading"),
62
62
  "polymarket_us_exchange": ("..trading.polymarket_us_exchange", "PolymarketUSExchangeTrading"),
63
+ "opinion": ("..trading.opinion", "OpinionTrading"),
63
64
  }
64
65
 
65
66
 
@@ -71,6 +71,13 @@ def default_streams(adapters: Mapping[str, Any], credentials: Mapping[str, Any])
71
71
  elif venue == "polymarket_us" and creds is not None:
72
72
  from ..ws.polymarket_us import PolymarketUSMarketStream, PolymarketUSPrivateStream
73
73
  out[venue] = VenueStreams(market=PolymarketUSMarketStream(creds), private=PolymarketUSPrivateStream(creds))
74
+ elif venue == "opinion" and creds is not None:
75
+ from ..ws.opinion import OpinionMarketStream, OpinionUserStream
76
+ catalog = getattr(adapter, "catalog", None)
77
+ out[venue] = VenueStreams(
78
+ market=OpinionMarketStream(api_key=creds.api_key, catalog=catalog),
79
+ private=OpinionUserStream(api_key=creds.api_key, catalog=catalog),
80
+ )
74
81
  else:
75
82
  log.info("synpath.engine.feeds: no stream for %s; its orders reach the engine by polling", venue)
76
83
  return out