skewtape 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.
- skewtape-0.1.3/.github/workflows/ci.yml +41 -0
- skewtape-0.1.3/.github/workflows/publish.yml +98 -0
- skewtape-0.1.3/.gitignore +7 -0
- skewtape-0.1.3/AGENTS.md +107 -0
- skewtape-0.1.3/LICENSE +21 -0
- skewtape-0.1.3/PKG-INFO +189 -0
- skewtape-0.1.3/README.md +174 -0
- skewtape-0.1.3/conformance/backtest.json +4740 -0
- skewtape-0.1.3/conformance/fees.json +642 -0
- skewtape-0.1.3/conformance/quote.json +3165 -0
- skewtape-0.1.3/notebooks/01_parity_spread.ipynb +358 -0
- skewtape-0.1.3/notebooks/02_longshot_pattern.ipynb +237 -0
- skewtape-0.1.3/notebooks/03_maker_rebates.ipynb +348 -0
- skewtape-0.1.3/notebooks/charts.py +683 -0
- skewtape-0.1.3/pyproject.toml +52 -0
- skewtape-0.1.3/src/skewtape/__init__.py +122 -0
- skewtape-0.1.3/src/skewtape/_generated/__init__.py +9 -0
- skewtape-0.1.3/src/skewtape/_generated/api/__init__.py +2 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/__init__.py +2 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/connect_feed.py +491 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/get_market.py +217 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/get_matched_event.py +199 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/get_pair.py +203 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/get_quote_context.py +227 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_bundles.py +215 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_candles.py +366 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_fees.py +229 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_fills.py +353 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_markets.py +275 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_matched_events.py +287 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_pairs.py +279 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_resolutions.py +267 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/list_venues.py +175 -0
- skewtape-0.1.3/src/skewtape/_generated/api/data/post_download_url.py +276 -0
- skewtape-0.1.3/src/skewtape/_generated/client.py +281 -0
- skewtape-0.1.3/src/skewtape/_generated/errors.py +17 -0
- skewtape-0.1.3/src/skewtape/_generated/models/__init__.py +124 -0
- skewtape-0.1.3/src/skewtape/_generated/models/bundle.py +100 -0
- skewtape-0.1.3/src/skewtape/_generated/models/bundle_file.py +78 -0
- skewtape-0.1.3/src/skewtape/_generated/models/bundle_list.py +95 -0
- skewtape-0.1.3/src/skewtape/_generated/models/candle.py +171 -0
- skewtape-0.1.3/src/skewtape/_generated/models/candle_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/candle_res.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/download_url.py +74 -0
- skewtape-0.1.3/src/skewtape/_generated/models/download_url_request.py +62 -0
- skewtape-0.1.3/src/skewtape/_generated/models/error_body.py +202 -0
- skewtape-0.1.3/src/skewtape/_generated/models/error_code.py +18 -0
- skewtape-0.1.3/src/skewtape/_generated/models/error_envelope.py +68 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee.py +196 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee_formula.py +12 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee_maker_rebate_params.py +53 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee_rounding.py +12 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fee_taker_params.py +53 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fill.py +279 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fill_leg.py +10 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fill_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fill_match_model.py +10 -0
- skewtape-0.1.3/src/skewtape/_generated/models/fill_side.py +10 -0
- skewtape-0.1.3/src/skewtape/_generated/models/list_candles_res.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/list_fills_leg.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/list_matched_events_status.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/list_pairs_status.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/locked_bundle.py +81 -0
- skewtape-0.1.3/src/skewtape/_generated/models/market.py +449 -0
- skewtape-0.1.3/src/skewtape/_generated/models/market_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/market_source_kind.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/market_status.py +12 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event.py +263 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event_member.py +207 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event_member_outcome_map.py +53 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event_member_status.py +10 -0
- skewtape-0.1.3/src/skewtape/_generated/models/matched_event_status.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/outcome.py +70 -0
- skewtape-0.1.3/src/skewtape/_generated/models/pair.py +183 -0
- skewtape-0.1.3/src/skewtape/_generated/models/pair_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/pair_member.py +112 -0
- skewtape-0.1.3/src/skewtape/_generated/models/pair_member_outcome_map.py +53 -0
- skewtape-0.1.3/src/skewtape/_generated/models/pair_status.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/plan_id.py +12 -0
- skewtape-0.1.3/src/skewtape/_generated/models/quote_context.py +124 -0
- skewtape-0.1.3/src/skewtape/_generated/models/quote_context_member.py +253 -0
- skewtape-0.1.3/src/skewtape/_generated/models/quote_context_member_fee_per.py +10 -0
- skewtape-0.1.3/src/skewtape/_generated/models/quote_context_member_fee_status.py +12 -0
- skewtape-0.1.3/src/skewtape/_generated/models/quote_context_member_outcome_map.py +54 -0
- skewtape-0.1.3/src/skewtape/_generated/models/resolution.py +222 -0
- skewtape-0.1.3/src/skewtape/_generated/models/resolution_event_type.py +15 -0
- skewtape-0.1.3/src/skewtape/_generated/models/resolution_list.py +91 -0
- skewtape-0.1.3/src/skewtape/_generated/models/resolution_source_kind.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue.py +122 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue_coverage.py +129 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue_kind_rights.py +114 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue_kind_rights_kind.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue_kind_rights_rights.py +11 -0
- skewtape-0.1.3/src/skewtape/_generated/models/venue_list.py +86 -0
- skewtape-0.1.3/src/skewtape/_generated/types.py +55 -0
- skewtape-0.1.3/src/skewtape/_keccak.py +66 -0
- skewtape-0.1.3/src/skewtape/_msgpack.py +97 -0
- skewtape-0.1.3/src/skewtape/backtest.py +697 -0
- skewtape-0.1.3/src/skewtape/client.py +614 -0
- skewtape-0.1.3/src/skewtape/download.py +159 -0
- skewtape-0.1.3/src/skewtape/errors.py +153 -0
- skewtape-0.1.3/src/skewtape/fees.py +248 -0
- skewtape-0.1.3/src/skewtape/geoblock.py +80 -0
- skewtape-0.1.3/src/skewtape/kalshi.py +574 -0
- skewtape-0.1.3/src/skewtape/paginated.py +58 -0
- skewtape-0.1.3/src/skewtape/quote.py +335 -0
- skewtape-0.1.3/src/skewtape/quote_math.py +561 -0
- skewtape-0.1.3/src/skewtape/route.py +520 -0
- skewtape-0.1.3/src/skewtape/route_hyperliquid.py +417 -0
- skewtape-0.1.3/src/skewtape/version.py +5 -0
- skewtape-0.1.3/testdata/candles.json +21 -0
- skewtape-0.1.3/testdata/error_forbidden_plan.json +12 -0
- skewtape-0.1.3/testdata/fees.json +23 -0
- skewtape-0.1.3/testdata/fills.json +28 -0
- skewtape-0.1.3/testdata/markets.json +69 -0
- skewtape-0.1.3/testdata/matched_event.json +55 -0
- skewtape-0.1.3/testdata/matched_events.json +60 -0
- skewtape-0.1.3/testdata/quote_context.json +76 -0
- skewtape-0.1.3/testdata/resolutions.json +19 -0
- skewtape-0.1.3/testdata/venues.json +33 -0
- skewtape-0.1.3/testdata-kalshi/README.md +15 -0
- skewtape-0.1.3/testdata-kalshi/candlesticks.json +140 -0
- skewtape-0.1.3/testdata-kalshi/error_404_flat.json +4 -0
- skewtape-0.1.3/testdata-kalshi/error_429.json +6 -0
- skewtape-0.1.3/testdata-kalshi/market.json +54 -0
- skewtape-0.1.3/testdata-kalshi/markets_page1.json +161 -0
- skewtape-0.1.3/testdata-kalshi/markets_page2.json +105 -0
- skewtape-0.1.3/testdata-kalshi/orderbook.json +24 -0
- skewtape-0.1.3/testdata-kalshi/trades_page1.json +65 -0
- skewtape-0.1.3/testdata-kalshi/trades_page2.json +41 -0
- skewtape-0.1.3/tests/conftest.py +247 -0
- skewtape-0.1.3/tests/hyperliquid_vectors.json +42 -0
- skewtape-0.1.3/tests/skewtape_values.json +11 -0
- skewtape-0.1.3/tests/test_agents.py +57 -0
- skewtape-0.1.3/tests/test_backtest.py +283 -0
- skewtape-0.1.3/tests/test_client.py +224 -0
- skewtape-0.1.3/tests/test_download.py +173 -0
- skewtape-0.1.3/tests/test_fees.py +77 -0
- skewtape-0.1.3/tests/test_geoblock.py +88 -0
- skewtape-0.1.3/tests/test_kalshi.py +390 -0
- skewtape-0.1.3/tests/test_no_skewtape_values.py +85 -0
- skewtape-0.1.3/tests/test_quote.py +415 -0
- skewtape-0.1.3/tests/test_readme.py +160 -0
- skewtape-0.1.3/tests/test_route.py +291 -0
- skewtape-0.1.3/tests/test_route_hyperliquid.py +293 -0
- skewtape-0.1.3/uv.lock +1280 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# skewtape-python's tests on every push to main and every pull request: the
|
|
2
|
+
# client against a mock API from testdata/, on the floor Python (3.11) and
|
|
3
|
+
# the newest. This file is kept in the tape repository
|
|
4
|
+
# (sdk/python/.github/workflows) and mirrored here by each release
|
|
5
|
+
# (scripts/sdk-release.sh): change it there.
|
|
6
|
+
|
|
7
|
+
name: ci
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ci-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
test:
|
|
23
|
+
name: tests (Python ${{ matrix.python }})
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 15
|
|
26
|
+
strategy:
|
|
27
|
+
fail-fast: false
|
|
28
|
+
matrix:
|
|
29
|
+
python: ["3.11", "3.13"]
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
|
|
35
|
+
- name: Set up uv
|
|
36
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
37
|
+
with:
|
|
38
|
+
version: "0.9.7"
|
|
39
|
+
|
|
40
|
+
- name: Tests
|
|
41
|
+
run: uv run --locked --python "${{ matrix.python }}" --group dev pytest -q
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Publish skewtape to PyPI on the vX.Y.Z tag that tape's release pushes here
|
|
2
|
+
# (tape's release-sdks.yml and scripts/sdk-release.sh), by trusted
|
|
3
|
+
# publishing: no token is stored anywhere. PyPI trusts this workflow file, in
|
|
4
|
+
# this repository, in the pypi environment. The build job tests and builds
|
|
5
|
+
# with no token in reach; only the publish job may ask GitHub for the OIDC
|
|
6
|
+
# token, and it runs no code of the package's. This file is kept in the tape
|
|
7
|
+
# repository (sdk/python/.github/workflows) and mirrored here by each
|
|
8
|
+
# release: change it there.
|
|
9
|
+
#
|
|
10
|
+
# TODO(decision): Olivier adds the trusted publisher on PyPI (for a new
|
|
11
|
+
# project, a pending publisher: project name skewtape, owner skewtape,
|
|
12
|
+
# repository skewtape-python, workflow publish.yml, environment pypi), and
|
|
13
|
+
# decides the package name. The licence is MIT (LICENSE, mirrored with the
|
|
14
|
+
# package).
|
|
15
|
+
|
|
16
|
+
name: publish
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
tags:
|
|
21
|
+
- "v*.*.*"
|
|
22
|
+
# A tag whose push started no run (api/v0.1.2, 15 Nov 2026) is published by
|
|
23
|
+
# hand on that tag: gh workflow run publish.yml --ref vX.Y.Z. A run on a
|
|
24
|
+
# branch stops at the version check.
|
|
25
|
+
workflow_dispatch:
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
|
|
30
|
+
concurrency:
|
|
31
|
+
group: publish-${{ github.ref }}
|
|
32
|
+
cancel-in-progress: false
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
build:
|
|
36
|
+
name: test and build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
timeout-minutes: 15
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
|
|
44
|
+
- name: Set up uv
|
|
45
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
46
|
+
with:
|
|
47
|
+
version: "0.9.7"
|
|
48
|
+
|
|
49
|
+
# The release sets the version module from the same X.Y.Z it tags.
|
|
50
|
+
- name: The tag is the package's version
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
if [ "${GITHUB_REF_TYPE}" != "tag" ]; then
|
|
54
|
+
echo "::error::run it on a vX.Y.Z tag: gh workflow run publish.yml --ref vX.Y.Z"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
want="${GITHUB_REF_NAME#v}"
|
|
58
|
+
got="$(sed -nE 's/^__version__ = "([^"]+)"$/\1/p' src/skewtape/version.py)"
|
|
59
|
+
if [ "${got}" != "${want}" ]; then
|
|
60
|
+
echo "::error::the tag is ${GITHUB_REF_NAME} but src/skewtape/version.py says ${got:-nothing}"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
- name: Tests
|
|
65
|
+
run: uv run --locked --python 3.11 --group dev pytest -q
|
|
66
|
+
|
|
67
|
+
- name: Build the wheel and the sdist
|
|
68
|
+
run: uv build --out-dir dist
|
|
69
|
+
|
|
70
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
71
|
+
with:
|
|
72
|
+
name: dist
|
|
73
|
+
path: dist/
|
|
74
|
+
if-no-files-found: error
|
|
75
|
+
retention-days: 7
|
|
76
|
+
|
|
77
|
+
publish:
|
|
78
|
+
name: publish to PyPI
|
|
79
|
+
needs: [build]
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
timeout-minutes: 10
|
|
82
|
+
environment:
|
|
83
|
+
name: pypi
|
|
84
|
+
url: https://pypi.org/project/skewtape/
|
|
85
|
+
permissions:
|
|
86
|
+
id-token: write
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
89
|
+
with:
|
|
90
|
+
name: dist
|
|
91
|
+
path: dist/
|
|
92
|
+
|
|
93
|
+
# Attestations sign each file in the public Sigstore log, with this
|
|
94
|
+
# repository's name: only once the repository is public.
|
|
95
|
+
- name: Publish
|
|
96
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
97
|
+
with:
|
|
98
|
+
attestations: ${{ !github.event.repository.private }}
|
skewtape-0.1.3/AGENTS.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- Rendered from tape's sdk/agents/AGENTS.md.tmpl by scripts/sdk-agents.py. Do not edit: change the template in tape. -->
|
|
2
|
+
|
|
3
|
+
# AGENTS.md: skewtape (Python)
|
|
4
|
+
|
|
5
|
+
For a coding agent working in this repository, or with the `skewtape` package in another one. Skewtape serves prediction-market history from every venue it covers in one schema: markets, fills, candles, fees and resolutions, and matched events that link the same question across venues. This package is the Python client of its API ([docs.skewtape.com](https://docs.skewtape.com)).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install skewtape
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.11 or later. The dependencies are httpx and attrs; `pip install 'skewtape[kalshi]'` adds what signing requests with a Kalshi key needs.
|
|
14
|
+
|
|
15
|
+
## Authenticate
|
|
16
|
+
|
|
17
|
+
Every call sends the key in the `X-API-Key` header. The client reads it from `SKEWTAPE_API_KEY`, or takes it as an argument. Keys start with `st_live_` and come from the dashboard ([docs.skewtape.com/keys](https://docs.skewtape.com/keys)). Never commit a key, print it or put it in a URL.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export SKEWTAPE_API_KEY=st_live_...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import datetime as dt
|
|
25
|
+
|
|
26
|
+
import skewtape
|
|
27
|
+
|
|
28
|
+
sk = skewtape.Skewtape() # the key from SKEWTAPE_API_KEY, or skewtape.Skewtape(api_key=...)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The five most common calls
|
|
32
|
+
|
|
33
|
+
The examples use a dated window, 2 to 3 November 2026: keep examples static and dated, never live.
|
|
34
|
+
|
|
35
|
+
1. **Matched events**: the same question across venues, with a confidence and whether every member resolves the same way. One by id with `get`.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
for event in sk.events(min_confidence=0.9, resolution_equal=True):
|
|
39
|
+
print(event.event_id, [m.venue for m in event.members])
|
|
40
|
+
event = sk.events.get("st_evt_00412")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. **Markets** of one venue, with their outcomes, status and `fee_category`. Iterating a list reads every page, one call each against the plan's monthly calls, so read the pages you need: this reads the first.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
page = next(sk.markets(venue="kalshi").pages())
|
|
47
|
+
for market in page:
|
|
48
|
+
print(market.market_id, market.status, market.fee_category)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. **Fills** of a matched event (or of a venue and market) over a window, maker legs by default. Sizes and notionals are exact integers in raw units: divide by `10 ** decimals`. Fills are on the Builder and Pro plans.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
since = dt.datetime(2026, 11, 2, tzinfo=dt.timezone.utc)
|
|
55
|
+
until = dt.datetime(2026, 11, 3, tzinfo=dt.timezone.utc)
|
|
56
|
+
for fill in sk.fills(event_id="st_evt_00412", since=since, until=until):
|
|
57
|
+
print(fill.venue, fill.ts, fill.price, fill.size_raw, fill.decimals)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. **Candles** of one size (1m, 1h or 1d) over the same window. Builder and Pro read every venue at every size; Free and History read Polymarket `1d` candles only, so this `1h` read is `403 forbidden_plan` there. The generated model names the open price `open_`, since `open` is a Python builtin.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
for candle in sk.candles(res="1h", event_id="st_evt_00412", since=since, until=until):
|
|
64
|
+
print(candle.ts, candle.open_, candle.close, candle.volume)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
5. **Fee schedules**, to net fees out of a fill: the row in force for a market's `fee_category` at the fill's time, and the fee it gives.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
rows = list(sk.fees(venue="kalshi"))
|
|
71
|
+
row = skewtape.select_fee_row(rows, "kalshi", "quadratic:1", since)
|
|
72
|
+
if row is not None:
|
|
73
|
+
print(row.category, skewtape.taker_fee_raw(row, 10_000_000_000, 4_000_000_000, 8))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Every list pages itself: iterate for every item, `.pages()` for the pages, `.first()` for the first item. Nothing is fetched until you iterate.
|
|
77
|
+
|
|
78
|
+
## More
|
|
79
|
+
|
|
80
|
+
- `sk.venues()`: the venues and their flags. `sk.resolutions(venue=..., market_id=[...])`: how and when each market resolved, of up to 100 markets at a time. `sk.pairs(...)`: matched events with exactly two active members.
|
|
81
|
+
- `sk.quote(event_id, outcome=..., side=..., size=...)`: the best venue, or a split across venues, for a size of one outcome of a matched event once each venue's taker fee is counted. One metered call reads the quote context; the books are read from each venue by a client that never carries the key. It places nothing.
|
|
82
|
+
- `sk.backtest(trades)`: fee-aware P&L for trades you supply, over the recorded history. It is arithmetic on history: it forecasts nothing, suggests no trade and optimises nothing. A trade no fee row covers is refused: give its `fee_category` on the trade, or pass `on_missing_fee="zero"` to count its fee as zero with a warning.
|
|
83
|
+
- `sk.download(url, dest)` and `sk.download.verify(folder)`: bundle files, resumed and checked against their sha256.
|
|
84
|
+
- `sk.kalshi()`: an optional read-only client of Kalshi's own API, under your own Kalshi membership. It is not how Skewtape's Kalshi data is delivered.
|
|
85
|
+
- `skewtape.route(venue=..., ...)` (also `sk.route`): an unsigned order for Polymarket, DFlow or Hyperliquid with your own builder code or fee. It never signs, never sends and never takes a private key; you sign and send it. `skewtape.geoblock.polymarket()` asks Polymarket whether it takes orders from this machine's location: one indication, not a licence.
|
|
86
|
+
|
|
87
|
+
## Errors and limits
|
|
88
|
+
|
|
89
|
+
A `429 rate_limited` is retried after the server's `Retry-After`; a `429 lookup_rate_limited` (too many invalid keys from the address) is not, and raises `AuthenticationError`: fix the key. Every other error raises a `SkewtapeError` subclass with the API's `code`, `message`, `docs_url` and `request_id`: `ForbiddenPlanError` names the plan a route needs, `QuotaExceededError` says when the monthly quota resets. Quote the `request_id` to support.
|
|
90
|
+
|
|
91
|
+
## Generated code
|
|
92
|
+
|
|
93
|
+
`src/skewtape/_generated/` is generated from Skewtape's public OpenAPI document by pinned generators, in the tape repository, and replaced whole on every release. Never edit it: a change there is lost and fails the release's check. Hand-written code lives beside it (`src/skewtape/client.py`, `quote.py`, `quote_math.py`, `route.py`, `route_hyperliquid.py`, `geoblock.py`, `backtest.py`, `fees.py`, `kalshi.py`, `download.py`, `errors.py`); a change the API needs goes to the API first. This file is rendered from a template in tape too.
|
|
94
|
+
|
|
95
|
+
## Rules
|
|
96
|
+
|
|
97
|
+
- **No custody.** Skewtape never holds funds, wallets, keys to them or orders. This package reads data and builds unsigned orders (`route`): it has no call that places an order, moves funds or signs a transaction. `sk.kalshi()` signs read requests with your own Kalshi key, which stays on your machine. Do not add code that sends funds or orders through Skewtape.
|
|
98
|
+
- **Static, dated examples.** Examples and docs use dated windows and recorded data, never live prices.
|
|
99
|
+
- **Copy.** Say "matched event" (a pair is a matched event with exactly two active members).
|
|
100
|
+
|
|
101
|
+
## Tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv run --locked --group dev pytest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The tests run against a mock API and recorded data in `testdata/` and `conformance/`; they make no network call. The code blocks of this file are among them.
|
skewtape-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blockbrainers SAS (Skewtape)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
skewtape-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: skewtape
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Skewtape API client: prediction-market data across venues in one schema, with matched events.
|
|
5
|
+
Project-URL: Documentation, https://docs.skewtape.com
|
|
6
|
+
Project-URL: Source, https://github.com/skewtape/skewtape-python
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: attrs>=23.1
|
|
11
|
+
Requires-Dist: httpx<1,>=0.27
|
|
12
|
+
Provides-Extra: kalshi
|
|
13
|
+
Requires-Dist: cryptography>=42; extra == 'kalshi'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# skewtape
|
|
17
|
+
|
|
18
|
+
Prediction-market data from every venue Skewtape serves, in one schema: markets, fills, candles, fees and resolutions, with matched events that link the same question across venues. This is the Python client for the [Skewtape API](https://docs.skewtape.com).
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install skewtape
|
|
22
|
+
export SKEWTAPE_API_KEY=st_live_... # a key from the dashboard, docs.skewtape.com/keys
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import skewtape
|
|
27
|
+
|
|
28
|
+
sk = skewtape.Skewtape() # or Skewtape(api_key="st_live_...")
|
|
29
|
+
|
|
30
|
+
for venue in sk.venues():
|
|
31
|
+
print(venue.key, venue.flags)
|
|
32
|
+
|
|
33
|
+
# Matched events: the same question across venues, with a confidence and
|
|
34
|
+
# whether every member resolves the same way.
|
|
35
|
+
for event in sk.events(min_confidence=0.9, resolution_equal=True):
|
|
36
|
+
print(event.event_id, event.question_canonical, [m.venue for m in event.members])
|
|
37
|
+
|
|
38
|
+
event = sk.events.get("st_evt_00412")
|
|
39
|
+
|
|
40
|
+
# Markets of one venue. Iterating a list reads every page, one call each
|
|
41
|
+
# against your plan's monthly calls, so this reads the first page only.
|
|
42
|
+
page = next(sk.markets(venue="kalshi").pages())
|
|
43
|
+
for market in page:
|
|
44
|
+
print(market.market_id, market.status, market.fee_category)
|
|
45
|
+
|
|
46
|
+
# Fills of a matched event over a dated window (maker legs by default).
|
|
47
|
+
# Fills are on the Builder and Pro plans: a Free or History key gets a
|
|
48
|
+
# ForbiddenPlanError here.
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
|
|
51
|
+
since = datetime(2026, 11, 2, tzinfo=timezone.utc)
|
|
52
|
+
until = datetime(2026, 11, 3, tzinfo=timezone.utc)
|
|
53
|
+
for fill in sk.fills(event_id="st_evt_00412", since=since, until=until):
|
|
54
|
+
print(fill.venue, fill.ts, fill.price)
|
|
55
|
+
|
|
56
|
+
# Pairs: matched events with exactly two active members.
|
|
57
|
+
pairs = list(sk.pairs(min_confidence=0.9))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The quickstart above is run against a mock API by the package's tests, so it stays current.
|
|
61
|
+
|
|
62
|
+
- Every list (`markets`, `events`, `pairs`, `fills`, `candles`, `resolutions`, `fees`) returns a `Paginated`: iterate for every item across pages, `.pages()` for the pages, `.first()` for the first item. Nothing is fetched until you iterate. `markets(resolved=...)` filters on the client, so every page is still fetched: pass a venue or market ids with it.
|
|
63
|
+
- A `429 rate_limited`, or a `429` with no error body as a proxy sends, is retried with the server's `Retry-After` (three times by default, `max_retries=`). A `429 lookup_rate_limited` (too many invalid keys from your address) raises `AuthenticationError` at once: fix the key. A `429 quota_exceeded` raises `QuotaExceededError` at once, with `resets_at` and `upgrade_url`. Any other error raises a `SkewtapeError` subclass (`AuthenticationError`, `ForbiddenPlanError` with `.plan`, `BlockedError` with `.block_reason_code`, `NotFoundError`, `RateLimitError`, `QuotaExceededError`, `ServerError`), carrying the API's `code`, `message`, `docs_url` and `request_id`.
|
|
64
|
+
- `sk.download.bundles()` lists the History snapshots and monthly deltas your account may download, and `sk.download.bundle(bundle_id, folder)` fetches one whole, resuming a partial download, checking each file's sha256 and the folder's `CHECKSUMS.sha256`; `sk.download.file(bundle_id, path, dest)` fetches one file. They need a key on the Builder or Pro plan (a History purchase downloads from the dashboard), and the file host never sees the key. `sk.download(url, dest, sha256=...)` fetches a link you already hold; `sk.download.verify(folder)` checks a folder.
|
|
65
|
+
- Withheld fields read `None` with `blocked` true and `block_reason_code` on the row: see [Data rights and geoblocking](https://docs.skewtape.com/rights).
|
|
66
|
+
- `sk.quote(event_id, outcome=..., side=..., size=...)` is the best venue, or split across venues, for a size of one outcome of a matched event once each venue's taker fee is counted ([Quotes](https://docs.skewtape.com/quote)). It places nothing. One metered call reads the quote context; the books are read from each venue, from your machine, by a client that never carries your key. Amounts are integers in millionths (`net_micros`, `net_avg_price_micros`, ...). A venue that cannot be reached is left out and named in `skipped`, and none reachable raises `VenueUnreachableError`; members that do not resolve alike raise `RuleMismatchError` unless `allow_rule_mismatch=True`; a venue whose fee is unknown (no fee row, or one the data policy withholds) is never compared with one whose fee is known: when a venue whose fee is known can fill some of the size, it is left out of the plan, quoted alone in `venues` before fees, and `warnings` says so (otherwise they are compared before fees, and `warnings` says the fee is not counted). Kalshi's book is Kalshi's public orderbook, read with no key (a `kalshi_dflow` member waits for DFlow access and is left out); any venue's reader can be replaced in `book_readers`.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# The best venue, or split, for 100 contracts of "yes" once each taker fee is
|
|
70
|
+
# counted: one metered call, then each venue's book. It places nothing.
|
|
71
|
+
q = sk.quote("st_evt_00412", outcome="yes", side="buy", size=100)
|
|
72
|
+
print(q.plan, q.filled_micro, q.net_avg_price_micros, [(leg.venue, leg.size_micro) for leg in q.legs])
|
|
73
|
+
for skip in q.skipped:
|
|
74
|
+
print("left out:", skip.venue, skip.reason, skip.detail)
|
|
75
|
+
for warning in q.warnings:
|
|
76
|
+
print("warning:", warning)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Backtest: fee-aware P&L on the record
|
|
80
|
+
|
|
81
|
+
`sk.backtest(trades)` replays trades you supply over the history Skewtape records and gives their P&L before and after fees. It is arithmetic on history: it forecasts nothing, suggests no trade and optimises nothing.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Two trades on a real Kalshi market that resolved yes on 3 Nov 2026; the prices are illustrative, not the market's.
|
|
85
|
+
trades = [
|
|
86
|
+
{"venue": "kalshi", "market_id": "KXKBORFI-26NOV030430SAMKTW-Y", "token_id": "yes", "side": "buy", "ts": "2026-11-02T10:00:00Z", "size": "100", "price": "0.40"},
|
|
87
|
+
{"venue": "kalshi", "market_id": "KXKBORFI-26NOV030430SAMKTW-Y", "token_id": "yes", "side": "sell", "ts": "2026-11-02T20:00:00Z", "size": "30", "price": "0.62"},
|
|
88
|
+
]
|
|
89
|
+
result = sk.backtest(trades, as_of=datetime(2026, 11, 4, tzinfo=timezone.utc))
|
|
90
|
+
print(result.totals.pnl, result.totals.pnl_before_fees, result.totals.fees)
|
|
91
|
+
for position in result.positions:
|
|
92
|
+
print(position.token_id, position.status, position.outcome, position.pnl_raw)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Prices.** A trade carries `price` (0 to 1) or an exact `notional_raw`, or neither: then the last fill (`history="fills"`, the default, Builder and Pro) or candle (`history="candles"`, `candle_res=`, `1h` by default; any size on Builder and Pro, Polymarket `1d` only on Free and History) at or before its time prices it. A candle prices a time only once its bucket has ended. `lookback` (a day by default) is how far back it looks, a candle more for candles. An open position at `as_of` is marked the same way. Trades that all carry a price, on markets resolved by `as_of`, read no fills or candles, so they run on every plan.
|
|
96
|
+
- **What it reads.** The fee rows of the trades' venues, their markets and those markets' resolutions (a request per 100 markets), and the history windows above. Pass any of them (`fees=`, `markets=`, `resolutions=`, `fills=`, `candles=`) to read nothing for it.
|
|
97
|
+
- **Fees.** The schedule row in force for the market's `fee_category` at the trade's time, as on [Fees](https://docs.skewtape.com/fees); `role="maker"` pays the maker fee where the venue charges one. A trade no row covers is refused, and the error names it. Where the data policy withholds a market's `fee_category` (the market row reads `blocked` true), give it on the trade (`fee_category=`), or pass `on_missing_fee="zero"` to count the fee as zero with a warning.
|
|
98
|
+
- **Value.** At `as_of` (now by default), each position is worth what its market resolved to (1 a share for the outcome it resolved to, 0 for the other), or, still open, the last price on record. A market whose current word is a reversal (it resolved, then the venue's word changed) is valued as open, at the last price on record, so it needs the history read on its window. A voided market is valued only with `void_payout=`, since what a venue pays on a void is its rule. Nothing after `as_of` is read.
|
|
99
|
+
- **Exact.** Amounts are integers in raw units (`totals.decimals`), prices exact fractions; `run_backtest(...)` is the same arithmetic over data you pass in, with no request. `conformance/backtest.json` pins it; the TypeScript SDK passes the same vectors, and every fee in them is checked against Skewtape's own fee code. `taker_fee_raw`, `maker_fee_raw` and `select_fee_row` are exported for fills you price yourself.
|
|
100
|
+
|
|
101
|
+
## Kalshi, live and optional
|
|
102
|
+
|
|
103
|
+
Skewtape's Kalshi data (markets, fills, candles, resolutions) comes from the calls above, under the Kalshi data licence. `sk.kalshi()` is something else: an optional live client that calls Kalshi's Trade API from your machine, under your own Kalshi membership, for a reading of the live venue beside that history. It is read only, writes nothing to disk, and never talks to a Skewtape host.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
kalshi = sk.kalshi() # public requests; with your own key: sk.kalshi(access_key=key_id, private_key=pem)
|
|
107
|
+
|
|
108
|
+
# The live Kalshi markets of a matched event, found by the ticker its kalshi
|
|
109
|
+
# and kalshi_dflow members share.
|
|
110
|
+
event = sk.events.get("st_evt_00412")
|
|
111
|
+
for market in kalshi.markets_for(event):
|
|
112
|
+
print(market.ticker, market.status, market.yes_bid_dollars, market.yes_ask_dollars)
|
|
113
|
+
|
|
114
|
+
book = kalshi.orderbook("KXKBORFI-26NOV030430SAMKTW-Y", depth=5)
|
|
115
|
+
print(book.best_yes_bid, book.best_no_bid)
|
|
116
|
+
|
|
117
|
+
for trade in kalshi.trades(ticker="KXKBORFI-26NOV030430SAMKTW-Y", since=since):
|
|
118
|
+
print(trade.created_time, trade.taker_outcome_side, trade.yes_price_dollars, trade.count_fp)
|
|
119
|
+
|
|
120
|
+
sticks = kalshi.candlesticks("KXKBORFI-26NOV030430SAMKTW-Y", period="1h", since=since)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `kalshi.markets(status=, series_ticker=, event_ticker=, tickers=)`, `kalshi.market(ticker)`, `kalshi.trades(ticker=, since=, until=)`, `kalshi.orderbook(ticker, depth=)` and `kalshi.candlesticks(ticker, period=, since=, until=)` (`1m`, `1h` or `1d`; a Kalshi `1d` stick is a New York day). `kalshi.tickers(event)` and `kalshi.markets_for(event)` read a matched event's `kalshi` and `kalshi_dflow` members, whose `market_id` is the Kalshi ticker.
|
|
124
|
+
- Prices and counts are Kalshi's own strings; every row keeps the whole JSON object in `.raw`.
|
|
125
|
+
- Kalshi serves older trades and settled markets from its `/historical` routes, past a cutoff that moves, and this client does not call them: for that history use `sk.fills` and `sk.markets`.
|
|
126
|
+
- Your key goes in the arguments, from wherever you keep it; the client reads no environment variable. With one, every request is signed as Kalshi's docs describe (Ed25519 or RSA; `pip install 'skewtape[kalshi]'` brings the signing) and counts against your account's rate-limit tier. Without one the calls are public requests. Kalshi's terms and rate limits apply to them, not your Skewtape plan.
|
|
127
|
+
- A `429` is retried with a backoff (0.5, 1 and 2 seconds); any other error raises `KalshiError`, a `SkewtapeError` with Kalshi's `code`, `message` and `details`.
|
|
128
|
+
|
|
129
|
+
## Notebooks
|
|
130
|
+
|
|
131
|
+
`notebooks/01_parity_spread.ipynb` measures the cross-venue parity spread on the free sample: for the members of a matched event that resolve alike, how far a Yes on one venue and a No on another stray from the one dollar they pay together, hour by hour, before and after each venue's taker fee. It reads the sample's Parquet files with pandas and makes no network call. The sample is not published yet ([docs.skewtape.com/history](https://docs.skewtape.com/history)); once it is, each sample ships the notebook too: unzip it and run the notebook there, or point `SKEWTAPE_SAMPLE_DIR` at the folder.
|
|
132
|
+
|
|
133
|
+
Two more read a History snapshot (the History plan's files, not open yet), with pandas and DuckDB, and make no network call. Run them in the snapshot's folder, or point `SKEWTAPE_HISTORY_DIR` at it:
|
|
134
|
+
|
|
135
|
+
- `notebooks/02_longshot_pattern.ipynb`: price and outcome. Every trade in a market with a known result, in ten-cent price bins, with how often its token paid, after Qin and Yang (arXiv 2606.04217, Table 3).
|
|
136
|
+
- `notebooks/03_maker_rebates.ipynb`: the fee table's maker rebates and maker fees, applied to every trade by venue, fee category and month, and the fee a venue reports set against the schedule's.
|
|
137
|
+
|
|
138
|
+
They describe what happened in the data; they are not forecasts. `pip install pandas pyarrow matplotlib duckdb notebook` runs all three.
|
|
139
|
+
|
|
140
|
+
`notebooks/charts.py` draws the website's three static charts from an unzipped bundle with this package: the parity spread of one matched event over time, a question two venues list under different rules, and the fee impact on a simple backtest. Each is an SVG and a PNG with the bundle's date in the image, and `charts.json` gives its caption and the code that draws it. It checks the bundle against its checksums, prices fees with the SDK's fee arithmetic, and makes no network call: `pip install skewtape pandas pyarrow matplotlib`, then `python notebooks/charts.py <unzipped bundle> <output folder>`. It holds every title, caption and rule note to the copy rules and stops on a break; `--title EVENT_ID=TEXT` and `--rule-note TEXT` replace the text.
|
|
141
|
+
|
|
142
|
+
`skewtape._generated` is generated from the public OpenAPI document and never edited by hand; the facade above is this package. `AGENTS.md` tells a coding agent how to use it. The version's major and minor follow the API's version.
|
|
143
|
+
|
|
144
|
+
## Routing an order with your own code
|
|
145
|
+
|
|
146
|
+
`skewtape.route(...)` (also `sk.route`) builds an unsigned order and stops there: it never signs, never sends, never takes a private key, and needs no Skewtape key. The builder code or fee it carries is yours; omit it and the order carries none. The package holds no Skewtape code or account, and a test keeps it that way.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import skewtape
|
|
150
|
+
|
|
151
|
+
# Polymarket: a V2 order to sign as EIP-712 typed data. exchange is the book's
|
|
152
|
+
# ("ctf", "neg_risk" or "protocol_v2"); tick_size is the book's tick.
|
|
153
|
+
order = skewtape.route(
|
|
154
|
+
venue="polymarket", exchange="ctf", token_id="71321045679252212594626385532706912750332728571942532289631379312455583992563",
|
|
155
|
+
side="BUY", price="0.52", size="10", tick_size="0.01", maker=your_address,
|
|
156
|
+
builder=your_builder_code, # optional: your bytes32 code from your Polymarket profile
|
|
157
|
+
)
|
|
158
|
+
signature = your_signer(order.typed_data) # your wallet signs; the SDK never sees a key
|
|
159
|
+
body = order.post_body(signature=signature, owner=your_clob_api_key_id) # POST /order, with your CLOB credentials
|
|
160
|
+
|
|
161
|
+
# DFlow (tokenized Kalshi outcome tokens): the GET /order request that returns
|
|
162
|
+
# the unsigned transaction, with your fee and fee account.
|
|
163
|
+
request = skewtape.route(
|
|
164
|
+
venue="kalshi_dflow", input_mint=usdc_mint, output_mint=outcome_mint, amount=25_000_000,
|
|
165
|
+
user_public_key=your_users_wallet, platform_fee_scale=50, fee_account=your_fee_token_account,
|
|
166
|
+
)
|
|
167
|
+
# GET request.full_url with your own DFlow key in the request.api_key_header header.
|
|
168
|
+
|
|
169
|
+
# Hyperliquid HIP-4 outcomes: an order on side 0 or 1 of an outcome (coin
|
|
170
|
+
# "#960" is outcome 96, side 0), signed by the key that trades (an API wallet).
|
|
171
|
+
hl = skewtape.route(
|
|
172
|
+
venue="hyperliquid", outcome=96, side=0, is_buy=False, price="0.55", size="20",
|
|
173
|
+
builder=your_builder_address, builder_fee=10, # optional, yours: 10 is 1 basis point
|
|
174
|
+
)
|
|
175
|
+
body = hl.post_body(your_api_wallet_signer(hl.typed_data)) # POST to hl.exchange_url
|
|
176
|
+
|
|
177
|
+
# Once per builder, the user's main wallet approves your maximum fee.
|
|
178
|
+
approval = skewtape.hyperliquid_approve_builder_fee(builder=your_builder_address, max_fee_rate="0.01%", signature_chain_id=42161)
|
|
179
|
+
# The wallet signs approval.typed_data; a signer that signs a hash signs approval.digest.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **Polymarket:** the order is the V2 `Order` (salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder), on the exchange's EIP-712 domain: "Polymarket CTF Exchange", version "2" for `ctf` and `neg_risk`, "3" for a Protocol V2 book (`protocol_v2`), Polygon. Amounts follow Polymarket's rounding by tick size. A limit order (`GTC`, `GTD` with `expiration` in Unix seconds) and a market SELL (`FOK`, `FAK`) take `size` in shares; a market BUY takes `amount`, the USD to spend, with `price` the most you pay. `signature_type` is `EOA` (the default; `signer` is the maker), `POLY_PROXY` or `POLY_GNOSIS_SAFE` (pass `signer`). A Deposit Wallet order (`POLY_1271`) is signed through an ERC-7739 wrapper this builder does not produce. Builder fee rates are set on your Polymarket profile, not in the order.
|
|
183
|
+
- **DFlow:** `platform_fee_scale` is DFlow's fee for a prediction market swap, 0 to 999 in thousandths; `platform_fee_bps` its fixed spot fee. Either needs `fee_account`, your token account for the fee mint, which must exist before the trade.
|
|
184
|
+
- **Hyperliquid (HIP-4 outcomes):** the order is an L1 action, signed as a "phantom agent" on the EIP-712 domain "Exchange", chain 1337, so it is signed by a key you hold (usually an API wallet your user approved), not a browser wallet on its own chain. `builder` and `builder_fee` (tenths of a basis point, at most 1000) are yours, both or neither; your user first approves your maximum fee with their main wallet (`hyperliquid_approve_builder_fee`). Prices have at most 5 significant figures and 8 decimals less `sz_decimals` (pass it if you have it: the docs do not give one for outcomes); `tif` is `Gtc`, `Ioc` or `Alo`; `nonce` is now in milliseconds by default. Hyperliquid's HIP-4 page says outcome fees are zero for testing and builders earn on sell orders, its fees page that fees are charged on closing or settling: the order is the same either way.
|
|
185
|
+
- **Your users are your responsibility:** who may trade on which venue, from where, under which terms. `skewtape.geoblock.polymarket()` asks Polymarket whether it would take orders from this machine's network location: `status` is `"allowed"`, `"blocked"` or `"unknown"`, and any failure (network, timeout, an unexpected reply) is `"unknown"`, never `"allowed"`. It is one indication, not a licence.
|
|
186
|
+
|
|
187
|
+
## Licence
|
|
188
|
+
|
|
189
|
+
MIT, copyright 2026 Blockbrainers SAS (Skewtape): see [LICENSE](LICENSE).
|