pmfees 0.1.0__py3-none-any.whl
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.
- pmfees/__init__.py +26 -0
- pmfees/__main__.py +3 -0
- pmfees/cli.py +177 -0
- pmfees/core.py +373 -0
- pmfees/data/prediction_market_fees.json +101 -0
- pmfees/py.typed +0 -0
- pmfees/rates.py +265 -0
- pmfees-0.1.0.dist-info/METADATA +232 -0
- pmfees-0.1.0.dist-info/RECORD +13 -0
- pmfees-0.1.0.dist-info/WHEEL +5 -0
- pmfees-0.1.0.dist-info/entry_points.txt +2 -0
- pmfees-0.1.0.dist-info/licenses/LICENSE +21 -0
- pmfees-0.1.0.dist-info/top_level.txt +1 -0
pmfees/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""pmfees: exact trading fees for Kalshi, Polymarket US and polymarket.com.
|
|
2
|
+
|
|
3
|
+
All rates, tiers, multipliers and rounding rules come from ``prediction_market_fees.json``
|
|
4
|
+
(bundled, or your own file via ``load_rates(path)`` / ``PMFEES_RATES_FILE``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .core import PLATFORMS, BreakEven, FeeResult, break_even, fee, platform_key
|
|
12
|
+
from .rates import Rates, RatesError, default_rates, load_rates
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PLATFORMS",
|
|
16
|
+
"BreakEven",
|
|
17
|
+
"FeeResult",
|
|
18
|
+
"Rates",
|
|
19
|
+
"RatesError",
|
|
20
|
+
"__version__",
|
|
21
|
+
"break_even",
|
|
22
|
+
"default_rates",
|
|
23
|
+
"fee",
|
|
24
|
+
"load_rates",
|
|
25
|
+
"platform_key",
|
|
26
|
+
]
|
pmfees/__main__.py
ADDED
pmfees/cli.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Command line: ``pmfees kalshi --price 0.50 --contracts 100``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .core import BreakEven, FeeResult, break_even, fee
|
|
13
|
+
from .rates import Rates, RatesError, load_rates
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _money(d: Decimal) -> str:
|
|
17
|
+
# Show at least cents, more when the platform rounds finer (polymarket.com: 5 decimals).
|
|
18
|
+
exp = d.normalize().as_tuple().exponent
|
|
19
|
+
places = max(2, -exp if isinstance(exp, int) else 2)
|
|
20
|
+
return f"${d:,.{places}f}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _price(d: Decimal) -> str:
|
|
24
|
+
return f"{(d * 100).normalize():f}¢"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_fee(r: FeeResult) -> str:
|
|
28
|
+
pct = r.fee_pct_of_trade_value
|
|
29
|
+
pct_s = "" if pct is None else f" ({pct.quantize(Decimal('0.01'))}% of trade value at {_price(r.price)})"
|
|
30
|
+
lines = [
|
|
31
|
+
f"{r.platform_name} {r.role} fee: {_money(r.fee)}{pct_s}",
|
|
32
|
+
f" {r.side} {r.contracts.normalize():f} contracts at {_money(r.price)}"
|
|
33
|
+
f" = {_money(r.trade_value)} trade value",
|
|
34
|
+
]
|
|
35
|
+
for k, v in r.details.items():
|
|
36
|
+
lines.append(f" {k.replace('_', ' ')}: {v}")
|
|
37
|
+
lines.append(
|
|
38
|
+
f" {r.coefficient.normalize():f} × {r.contracts.normalize():f} × {r.price.normalize():f}"
|
|
39
|
+
f" × (1 − {r.price.normalize():f}) = {r.raw_fee.normalize():f}, {r.rounding}: {_money(r.fee)}"
|
|
40
|
+
)
|
|
41
|
+
lines += [f" note: {n}" for n in r.notes]
|
|
42
|
+
lines.append(f" rates as of {r.rates_as_of} · source: {r.source_url}")
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _render_break_even(b: BreakEven) -> str:
|
|
47
|
+
verb = "rise" if b.side == "buy" else "fall"
|
|
48
|
+
if b.exit_price is None or b.move is None or b.exit_fee is None or b.profit_at_exit is None:
|
|
49
|
+
return (
|
|
50
|
+
f"Break-even: no exit price below $1 puts this {b.side} in profit after fees ({b.platform_name})."
|
|
51
|
+
)
|
|
52
|
+
return (
|
|
53
|
+
f"Break-even: the price must {verb} {abs(b.move * 100).normalize():f}¢ "
|
|
54
|
+
f"to {_price(b.exit_price)} (tick {_price(b.tick)}); "
|
|
55
|
+
f"entry fee {_money(b.entry_fee)} + exit fee {_money(b.exit_fee)}, "
|
|
56
|
+
f"profit at that price {_money(b.profit_at_exit)}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parser() -> argparse.ArgumentParser:
|
|
61
|
+
p = argparse.ArgumentParser(
|
|
62
|
+
prog="pmfees",
|
|
63
|
+
description="Exact trading fees for Kalshi, Polymarket US and polymarket.com.",
|
|
64
|
+
)
|
|
65
|
+
p.add_argument("--version", action="version", version=f"pmfees {__version__}")
|
|
66
|
+
sub = p.add_subparsers(dest="platform", required=True, metavar="PLATFORM")
|
|
67
|
+
|
|
68
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
69
|
+
common.add_argument("--price", required=True, help="contract price in dollars, e.g. 0.50")
|
|
70
|
+
common.add_argument("--contracts", required=True, help="number of contracts (shares)")
|
|
71
|
+
common.add_argument("--side", choices=("buy", "sell"), default="buy")
|
|
72
|
+
common.add_argument("--maker", action="store_true", help="maker order instead of taker")
|
|
73
|
+
common.add_argument("--break-even", action="store_true", help="also show the break-even price move")
|
|
74
|
+
common.add_argument("--tick", default="0.01", help="price step for --break-even (default 0.01)")
|
|
75
|
+
common.add_argument("--json", action="store_true", help="print JSON instead of text")
|
|
76
|
+
common.add_argument("--rates", help="path to an updated prediction_market_fees.json")
|
|
77
|
+
|
|
78
|
+
k = sub.add_parser("kalshi", parents=[common], help="Kalshi")
|
|
79
|
+
k.add_argument("--multiplier", help="fee multiplier M (default 1 for takers, 0 for makers)")
|
|
80
|
+
|
|
81
|
+
us = sub.add_parser("polymarket-us", parents=[common], help="Polymarket US")
|
|
82
|
+
grp = us.add_mutually_exclusive_group()
|
|
83
|
+
grp.add_argument("--tier", help="taker tier 1-4, or 'new' for the first 7 days (default 1)")
|
|
84
|
+
grp.add_argument("--volume", help="trailing 30-day taker volume in USD (picks the tier)")
|
|
85
|
+
|
|
86
|
+
g = sub.add_parser("polymarket", parents=[common], help="polymarket.com (international)")
|
|
87
|
+
g.add_argument("--category", required=True, help="market category, e.g. Crypto, Sports, Politics")
|
|
88
|
+
|
|
89
|
+
r = sub.add_parser("rates", help="show the loaded rates and their date")
|
|
90
|
+
r.add_argument("--rates", help="path to an updated prediction_market_fees.json")
|
|
91
|
+
return p
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _rates_summary(rates: Rates) -> str:
|
|
95
|
+
g, us, k = rates.polymarket_global, rates.polymarket_us, rates.kalshi
|
|
96
|
+
lines = [f"Rates current as of {rates.as_of} ({rates.path})", "", f"Kalshi ({k.source_url})"]
|
|
97
|
+
lines.append(
|
|
98
|
+
f" taker {k.taker_rate}, maker {k.maker_rate}, series multipliers "
|
|
99
|
+
f"{', '.join(str(m) for m in k.multipliers)}; {k.rounding.describe()}"
|
|
100
|
+
)
|
|
101
|
+
lines += ["", f"Polymarket US ({us.source_url}, effective {us.effective})"]
|
|
102
|
+
for t in us.tiers:
|
|
103
|
+
rng = f"below ${t.below:,}" if t.below is not None else "the rest"
|
|
104
|
+
lines.append(f" {t.label}: Θ {t.theta} (30-day taker volume {rng})")
|
|
105
|
+
lines.append(f" New account: Θ {us.new_account_theta}; {us.rounding.describe()}")
|
|
106
|
+
lines += ["", f"polymarket.com ({g.source_url})"]
|
|
107
|
+
lines += [f" {cat}: {rate}" for cat, rate in g.rates.items()]
|
|
108
|
+
lines.append(f" {g.rounding.describe()}")
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
113
|
+
args = _parser().parse_args(argv)
|
|
114
|
+
try:
|
|
115
|
+
rates = load_rates(args.rates)
|
|
116
|
+
if args.platform == "rates":
|
|
117
|
+
print(_rates_summary(rates))
|
|
118
|
+
return 0
|
|
119
|
+
opts: dict[str, str | None] = {
|
|
120
|
+
"multiplier": getattr(args, "multiplier", None),
|
|
121
|
+
"tier": getattr(args, "tier", None),
|
|
122
|
+
"volume_30d": getattr(args, "volume", None),
|
|
123
|
+
"category": getattr(args, "category", None),
|
|
124
|
+
}
|
|
125
|
+
result = fee(
|
|
126
|
+
args.platform,
|
|
127
|
+
args.price,
|
|
128
|
+
args.contracts,
|
|
129
|
+
args.side,
|
|
130
|
+
opts["tier"],
|
|
131
|
+
args.maker,
|
|
132
|
+
category=opts["category"],
|
|
133
|
+
multiplier=opts["multiplier"],
|
|
134
|
+
volume_30d=opts["volume_30d"],
|
|
135
|
+
rates=rates,
|
|
136
|
+
)
|
|
137
|
+
be = None
|
|
138
|
+
if args.break_even:
|
|
139
|
+
be = break_even(
|
|
140
|
+
args.platform,
|
|
141
|
+
args.price,
|
|
142
|
+
args.contracts,
|
|
143
|
+
args.side,
|
|
144
|
+
opts["tier"],
|
|
145
|
+
args.maker,
|
|
146
|
+
tick=args.tick,
|
|
147
|
+
category=opts["category"],
|
|
148
|
+
multiplier=opts["multiplier"],
|
|
149
|
+
volume_30d=opts["volume_30d"],
|
|
150
|
+
rates=rates,
|
|
151
|
+
)
|
|
152
|
+
except (ValueError, ArithmeticError, RatesError, OSError) as exc:
|
|
153
|
+
print(f"pmfees: error: {exc}", file=sys.stderr)
|
|
154
|
+
return 2
|
|
155
|
+
|
|
156
|
+
if args.json:
|
|
157
|
+
out: dict[str, object] = {"fee": result.breakdown(), "notes": list(result.notes)}
|
|
158
|
+
if be is not None:
|
|
159
|
+
out["break_even"] = {
|
|
160
|
+
"entry_price": str(be.entry_price),
|
|
161
|
+
"exit_price": None if be.exit_price is None else str(be.exit_price),
|
|
162
|
+
"move": None if be.move is None else str(be.move),
|
|
163
|
+
"entry_fee_usd": str(be.entry_fee),
|
|
164
|
+
"exit_fee_usd": None if be.exit_fee is None else str(be.exit_fee),
|
|
165
|
+
"profit_at_exit_usd": None if be.profit_at_exit is None else str(be.profit_at_exit),
|
|
166
|
+
"tick": str(be.tick),
|
|
167
|
+
}
|
|
168
|
+
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
169
|
+
else:
|
|
170
|
+
print(_render_fee(result))
|
|
171
|
+
if be is not None:
|
|
172
|
+
print(_render_break_even(be))
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
raise SystemExit(main())
|
pmfees/core.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Fee and break-even calculations for Kalshi, Polymarket US and polymarket.com."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from .rates import Rates, Rounding, default_rates
|
|
10
|
+
|
|
11
|
+
Platform = Literal["kalshi", "polymarket-us", "polymarket"]
|
|
12
|
+
Side = Literal["buy", "sell"]
|
|
13
|
+
Number = Decimal | int | float | str
|
|
14
|
+
|
|
15
|
+
PLATFORMS: tuple[Platform, ...] = ("kalshi", "polymarket-us", "polymarket")
|
|
16
|
+
|
|
17
|
+
_ALIASES: dict[str, Platform] = {
|
|
18
|
+
"kalshi": "kalshi",
|
|
19
|
+
"polymarket-us": "polymarket-us",
|
|
20
|
+
"polymarket_us": "polymarket-us",
|
|
21
|
+
"pm-us": "polymarket-us",
|
|
22
|
+
"polymarket": "polymarket",
|
|
23
|
+
"polymarket.com": "polymarket",
|
|
24
|
+
"polymarket-global": "polymarket",
|
|
25
|
+
"polymarket_global": "polymarket",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
DISPLAY_NAMES: dict[Platform, str] = {
|
|
29
|
+
"kalshi": "Kalshi",
|
|
30
|
+
"polymarket-us": "Polymarket US",
|
|
31
|
+
"polymarket": "polymarket.com",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ONE = Decimal(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _num(value: Number, what: str) -> Decimal:
|
|
38
|
+
if isinstance(value, bool):
|
|
39
|
+
raise TypeError(f"{what} must be a number")
|
|
40
|
+
# str() first so 0.1 (float) becomes Decimal("0.1"), not its binary expansion.
|
|
41
|
+
d = value if isinstance(value, Decimal) else Decimal(str(value))
|
|
42
|
+
if not d.is_finite():
|
|
43
|
+
raise ValueError(f"{what} must be finite")
|
|
44
|
+
return d
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def platform_key(platform: str) -> Platform:
|
|
48
|
+
key = _ALIASES.get(platform.strip().lower())
|
|
49
|
+
if key is None:
|
|
50
|
+
raise ValueError(f"unknown platform {platform!r}; use one of: {', '.join(PLATFORMS)}")
|
|
51
|
+
return key
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FeeResult:
|
|
56
|
+
"""A fee in USD with everything that went into it."""
|
|
57
|
+
|
|
58
|
+
platform: Platform
|
|
59
|
+
platform_name: str
|
|
60
|
+
side: Side
|
|
61
|
+
role: Literal["taker", "maker"]
|
|
62
|
+
price: Decimal
|
|
63
|
+
contracts: Decimal
|
|
64
|
+
coefficient: Decimal
|
|
65
|
+
"""The coefficient applied in coefficient × C × p × (1 − p), multipliers included."""
|
|
66
|
+
raw_fee: Decimal
|
|
67
|
+
"""Exact fee before the platform's rounding."""
|
|
68
|
+
fee: Decimal
|
|
69
|
+
"""Fee in USD after the platform's rounding (what you pay)."""
|
|
70
|
+
rounding: str
|
|
71
|
+
formula: str
|
|
72
|
+
source_url: str
|
|
73
|
+
rates_as_of: str
|
|
74
|
+
details: dict[str, str] = field(default_factory=dict)
|
|
75
|
+
notes: tuple[str, ...] = ()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def usd(self) -> float:
|
|
79
|
+
return float(self.fee)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def trade_value(self) -> Decimal:
|
|
83
|
+
"""C × p: what a buyer pays, or a seller receives, before fees."""
|
|
84
|
+
return self.contracts * self.price
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def fee_pct_of_trade_value(self) -> Decimal | None:
|
|
88
|
+
"""Fee as a percentage of the trade value at this price (None when the value is 0)."""
|
|
89
|
+
value = self.trade_value
|
|
90
|
+
return None if value == 0 else self.fee / value * 100
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def cash_flow(self) -> Decimal:
|
|
94
|
+
"""Signed cash for the trade including the fee: negative when you pay."""
|
|
95
|
+
return -(self.trade_value + self.fee) if self.side == "buy" else self.trade_value - self.fee
|
|
96
|
+
|
|
97
|
+
def breakdown(self) -> dict[str, str]:
|
|
98
|
+
"""Plain strings, ready for JSON."""
|
|
99
|
+
pct = self.fee_pct_of_trade_value
|
|
100
|
+
out = {
|
|
101
|
+
"platform": self.platform_name,
|
|
102
|
+
"side": self.side,
|
|
103
|
+
"role": self.role,
|
|
104
|
+
"price": str(self.price),
|
|
105
|
+
"contracts": str(self.contracts),
|
|
106
|
+
"trade_value_usd": str(self.trade_value),
|
|
107
|
+
"coefficient": str(self.coefficient),
|
|
108
|
+
"raw_fee_usd": str(self.raw_fee),
|
|
109
|
+
"fee_usd": str(self.fee),
|
|
110
|
+
"fee_pct_of_trade_value": "" if pct is None else str(pct.quantize(Decimal("0.0001"))),
|
|
111
|
+
"cash_flow_usd": str(self.cash_flow),
|
|
112
|
+
"rounding": self.rounding,
|
|
113
|
+
"formula": self.formula,
|
|
114
|
+
"source_url": self.source_url,
|
|
115
|
+
"rates_as_of": self.rates_as_of,
|
|
116
|
+
}
|
|
117
|
+
out.update(self.details)
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _quadratic(coefficient: Decimal, contracts: Decimal, price: Decimal) -> Decimal:
|
|
122
|
+
return coefficient * contracts * price * (ONE - price)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _us_tier(rates: Rates, tier: int | str | None, volume_30d: Number | None) -> tuple[Decimal, str]:
|
|
126
|
+
us = rates.polymarket_us
|
|
127
|
+
if tier is not None and volume_30d is not None:
|
|
128
|
+
raise ValueError("give either tier or volume_30d, not both")
|
|
129
|
+
if volume_30d is not None:
|
|
130
|
+
vol = _num(volume_30d, "volume_30d")
|
|
131
|
+
if vol < 0:
|
|
132
|
+
raise ValueError("volume_30d must be >= 0")
|
|
133
|
+
for t in us.tiers:
|
|
134
|
+
if t.below is None or vol < t.below:
|
|
135
|
+
return t.theta, f"{t.label} (30-day taker volume ${vol:,})"
|
|
136
|
+
if tier is None:
|
|
137
|
+
first = us.tiers[0]
|
|
138
|
+
return first.theta, f"{first.label} (default: the highest coefficient)"
|
|
139
|
+
if isinstance(tier, str) and tier.strip().lower() in ("new", "new-account", "new_account"):
|
|
140
|
+
return us.new_account_theta, f"New account ({us.new_account_note})"
|
|
141
|
+
try:
|
|
142
|
+
n = int(str(tier).strip().lower().removeprefix("tier").removeprefix("t").strip())
|
|
143
|
+
except ValueError:
|
|
144
|
+
n = 0
|
|
145
|
+
for t in us.tiers:
|
|
146
|
+
if t.number == n:
|
|
147
|
+
return t.theta, t.label
|
|
148
|
+
valid = ", ".join([str(t.number) for t in us.tiers] + ["new"])
|
|
149
|
+
raise ValueError(f"unknown Polymarket US tier {tier!r}; use one of: {valid}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _global_category(rates: Rates, category: str | None) -> tuple[str, Decimal]:
|
|
153
|
+
cats = rates.polymarket_global.rates
|
|
154
|
+
if category is None:
|
|
155
|
+
raise ValueError(f"polymarket.com needs a category; one of: {', '.join(cats)}")
|
|
156
|
+
for name, rate in cats.items():
|
|
157
|
+
if name.lower() == category.strip().lower():
|
|
158
|
+
return name, rate
|
|
159
|
+
raise ValueError(f"unknown polymarket.com category {category!r}; use one of: {', '.join(cats)}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def fee(
|
|
163
|
+
platform: str,
|
|
164
|
+
price: Number,
|
|
165
|
+
contracts: Number,
|
|
166
|
+
side: Side = "buy",
|
|
167
|
+
tier: int | str | None = None,
|
|
168
|
+
maker: bool = False,
|
|
169
|
+
*,
|
|
170
|
+
category: str | None = None,
|
|
171
|
+
multiplier: Number | None = None,
|
|
172
|
+
volume_30d: Number | None = None,
|
|
173
|
+
rates: Rates | None = None,
|
|
174
|
+
) -> FeeResult:
|
|
175
|
+
"""Trading fee for one order.
|
|
176
|
+
|
|
177
|
+
``price`` is the price of the contract you trade, in dollars (0 < price < 1).
|
|
178
|
+
``side`` only changes the cash flow; the fee formulas are symmetric.
|
|
179
|
+
|
|
180
|
+
Platform options:
|
|
181
|
+
|
|
182
|
+
* ``kalshi``: ``multiplier`` is the multiplier M. It defaults to 1 for takers
|
|
183
|
+
and 0 for makers (Kalshi charges makers only where the market says so, so pass
|
|
184
|
+
``multiplier=1`` with ``maker=True`` for those). The fee is rounded up so that
|
|
185
|
+
fee + position cost (C × P) is a whole centicent ($0.0001).
|
|
186
|
+
* ``polymarket-us``: ``tier`` is 1-4 or ``"new"`` (first 7 days), or give
|
|
187
|
+
``volume_30d`` (trailing 30-day taker volume in USD). Default: Tier 1.
|
|
188
|
+
Makers pay no fee.
|
|
189
|
+
* ``polymarket`` (polymarket.com): ``category`` is required. Makers pay no fee.
|
|
190
|
+
"""
|
|
191
|
+
r = rates if rates is not None else default_rates()
|
|
192
|
+
key = platform_key(platform)
|
|
193
|
+
p = _num(price, "price")
|
|
194
|
+
c = _num(contracts, "contracts")
|
|
195
|
+
if not ONE > p > 0:
|
|
196
|
+
raise ValueError("price must be between 0 and 1 (exclusive), in dollars")
|
|
197
|
+
if c <= 0:
|
|
198
|
+
raise ValueError("contracts must be > 0")
|
|
199
|
+
if side not in ("buy", "sell"):
|
|
200
|
+
raise ValueError("side must be 'buy' or 'sell'")
|
|
201
|
+
if key != "polymarket-us" and (tier is not None or volume_30d is not None):
|
|
202
|
+
raise ValueError("tier and volume_30d only apply to polymarket-us")
|
|
203
|
+
if key != "kalshi" and multiplier is not None:
|
|
204
|
+
raise ValueError("multiplier only applies to kalshi")
|
|
205
|
+
if key != "polymarket" and category is not None:
|
|
206
|
+
raise ValueError("category only applies to polymarket (polymarket.com)")
|
|
207
|
+
|
|
208
|
+
role: Literal["taker", "maker"] = "maker" if maker else "taker"
|
|
209
|
+
details: dict[str, str] = {}
|
|
210
|
+
notes: list[str] = []
|
|
211
|
+
rounding: Rounding
|
|
212
|
+
|
|
213
|
+
if key == "kalshi":
|
|
214
|
+
k = r.kalshi
|
|
215
|
+
default_m = Decimal(0) if maker else ONE
|
|
216
|
+
m = default_m if multiplier is None else _num(multiplier, "multiplier")
|
|
217
|
+
if m not in k.multipliers:
|
|
218
|
+
allowed = ", ".join(str(x) for x in k.multipliers)
|
|
219
|
+
raise ValueError(f"unknown Kalshi series multiplier {m}; the rates file lists: {allowed}")
|
|
220
|
+
base = k.maker_rate if maker else k.taker_rate
|
|
221
|
+
coefficient = m * base
|
|
222
|
+
details["base_rate"] = str(base)
|
|
223
|
+
details["multiplier"] = str(m)
|
|
224
|
+
if k.version:
|
|
225
|
+
details["fee_schedule_version"] = k.version
|
|
226
|
+
if maker and multiplier is None:
|
|
227
|
+
notes.append(
|
|
228
|
+
"Kalshi maker multiplier defaults to 0; pass multiplier=1 for markets with maker fees."
|
|
229
|
+
)
|
|
230
|
+
rounding, formula, source, as_of = k.rounding, k.formula, k.source_url, k.as_of
|
|
231
|
+
elif key == "polymarket-us":
|
|
232
|
+
us = r.polymarket_us
|
|
233
|
+
theta, label = _us_tier(r, tier, volume_30d)
|
|
234
|
+
details["tier"] = label
|
|
235
|
+
details["theta"] = str(theta)
|
|
236
|
+
if maker:
|
|
237
|
+
coefficient = Decimal(0)
|
|
238
|
+
notes.append(f"Makers: {us.makers}. Maker rebates are not modelled.")
|
|
239
|
+
else:
|
|
240
|
+
coefficient = theta
|
|
241
|
+
rounding, formula, source, as_of = us.rounding, us.formula, us.source_url, us.as_of
|
|
242
|
+
else:
|
|
243
|
+
g = r.polymarket_global
|
|
244
|
+
cat, rate = _global_category(r, category)
|
|
245
|
+
details["category"] = cat
|
|
246
|
+
details["category_rate"] = str(rate)
|
|
247
|
+
if maker:
|
|
248
|
+
coefficient = Decimal(0)
|
|
249
|
+
notes.append("polymarket.com: makers are never charged fees.")
|
|
250
|
+
else:
|
|
251
|
+
coefficient = rate
|
|
252
|
+
notes.append(
|
|
253
|
+
"Each market's own feeSchedule is authoritative; series can differ from the category rate."
|
|
254
|
+
)
|
|
255
|
+
rounding, formula, source, as_of = g.rounding, g.formula, g.source_url, g.as_of
|
|
256
|
+
|
|
257
|
+
raw = _quadratic(coefficient, c, p)
|
|
258
|
+
# Kalshi (basis fee_plus_position_cost) rounds fee + C × P; the others round the fee alone.
|
|
259
|
+
rounded, rounding_text = rounding.apply(raw, c * p), rounding.describe()
|
|
260
|
+
return FeeResult(
|
|
261
|
+
platform=key,
|
|
262
|
+
platform_name=DISPLAY_NAMES[key],
|
|
263
|
+
side=side,
|
|
264
|
+
role=role,
|
|
265
|
+
price=p,
|
|
266
|
+
contracts=c,
|
|
267
|
+
coefficient=coefficient,
|
|
268
|
+
raw_fee=raw,
|
|
269
|
+
fee=rounded,
|
|
270
|
+
rounding=rounding_text,
|
|
271
|
+
formula=formula,
|
|
272
|
+
source_url=source,
|
|
273
|
+
rates_as_of=as_of or r.as_of,
|
|
274
|
+
details=details,
|
|
275
|
+
notes=tuple(notes),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True)
|
|
280
|
+
class BreakEven:
|
|
281
|
+
"""The smallest price move that puts a round trip in profit after the fees on both orders."""
|
|
282
|
+
|
|
283
|
+
platform_name: str
|
|
284
|
+
side: Side
|
|
285
|
+
contracts: Decimal
|
|
286
|
+
entry_price: Decimal
|
|
287
|
+
entry_fee: Decimal
|
|
288
|
+
exit_price: Decimal | None
|
|
289
|
+
"""First price on the tick grid where the round trip is > 0 after fees (None: none exists)."""
|
|
290
|
+
exit_fee: Decimal | None
|
|
291
|
+
move: Decimal | None
|
|
292
|
+
"""exit_price − entry_price (positive for a buy, negative for a sell)."""
|
|
293
|
+
profit_at_exit: Decimal | None
|
|
294
|
+
tick: Decimal
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def move_cents(self) -> Decimal | None:
|
|
298
|
+
return None if self.move is None else self.move * 100
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def break_even(
|
|
302
|
+
platform: str,
|
|
303
|
+
price: Number,
|
|
304
|
+
contracts: Number,
|
|
305
|
+
side: Side = "buy",
|
|
306
|
+
tier: int | str | None = None,
|
|
307
|
+
maker: bool = False,
|
|
308
|
+
*,
|
|
309
|
+
tick: Number = "0.01",
|
|
310
|
+
category: str | None = None,
|
|
311
|
+
multiplier: Number | None = None,
|
|
312
|
+
volume_30d: Number | None = None,
|
|
313
|
+
rates: Rates | None = None,
|
|
314
|
+
) -> BreakEven:
|
|
315
|
+
"""How far the price must move before closing the position is in profit after fees.
|
|
316
|
+
|
|
317
|
+
Opens ``contracts`` at ``price`` (``side="buy"`` buys, ``"sell"`` sells) and closes
|
|
318
|
+
them at a later price on the ``tick`` grid, paying the fee on both orders with the
|
|
319
|
+
same role and options. Returns the first exit price where
|
|
320
|
+
proceeds − cost − entry fee − exit fee > 0.
|
|
321
|
+
"""
|
|
322
|
+
r = rates if rates is not None else default_rates()
|
|
323
|
+
t = _num(tick, "tick")
|
|
324
|
+
if t <= 0:
|
|
325
|
+
raise ValueError("tick must be > 0")
|
|
326
|
+
|
|
327
|
+
def leg(at: Number, leg_side: Side) -> FeeResult:
|
|
328
|
+
return fee(
|
|
329
|
+
platform,
|
|
330
|
+
at,
|
|
331
|
+
contracts,
|
|
332
|
+
leg_side,
|
|
333
|
+
tier,
|
|
334
|
+
maker,
|
|
335
|
+
category=category,
|
|
336
|
+
multiplier=multiplier,
|
|
337
|
+
volume_30d=volume_30d,
|
|
338
|
+
rates=r,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
entry = leg(price, side)
|
|
342
|
+
exit_side: Side = "sell" if side == "buy" else "buy"
|
|
343
|
+
step = t if side == "buy" else -t
|
|
344
|
+
q = entry.price + step
|
|
345
|
+
while ONE > q > 0:
|
|
346
|
+
ex = leg(q, exit_side)
|
|
347
|
+
profit = entry.cash_flow + ex.cash_flow
|
|
348
|
+
if profit > 0:
|
|
349
|
+
return BreakEven(
|
|
350
|
+
platform_name=entry.platform_name,
|
|
351
|
+
side=side,
|
|
352
|
+
contracts=entry.contracts,
|
|
353
|
+
entry_price=entry.price,
|
|
354
|
+
entry_fee=entry.fee,
|
|
355
|
+
exit_price=q,
|
|
356
|
+
exit_fee=ex.fee,
|
|
357
|
+
move=q - entry.price,
|
|
358
|
+
profit_at_exit=profit,
|
|
359
|
+
tick=t,
|
|
360
|
+
)
|
|
361
|
+
q += step
|
|
362
|
+
return BreakEven(
|
|
363
|
+
platform_name=entry.platform_name,
|
|
364
|
+
side=side,
|
|
365
|
+
contracts=entry.contracts,
|
|
366
|
+
entry_price=entry.price,
|
|
367
|
+
entry_fee=entry.fee,
|
|
368
|
+
exit_price=None,
|
|
369
|
+
exit_fee=None,
|
|
370
|
+
move=None,
|
|
371
|
+
profit_at_exit=None,
|
|
372
|
+
tick=t,
|
|
373
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"as_of": "2026-11-08",
|
|
3
|
+
"polymarket_global": {
|
|
4
|
+
"as_of": "2026-11-08",
|
|
5
|
+
"name": "Polymarket (international, polymarket.com)",
|
|
6
|
+
"source_url": "https://docs.polymarket.com/trading/fees",
|
|
7
|
+
"formula": "fee = C × feeRate × p × (1 − p); only takers pay; C = shares, p = share price",
|
|
8
|
+
"rates": {
|
|
9
|
+
"Crypto": 0.07,
|
|
10
|
+
"Sports": 0.05,
|
|
11
|
+
"Economics": 0.05,
|
|
12
|
+
"Culture": 0.05,
|
|
13
|
+
"Weather": 0.05,
|
|
14
|
+
"Other": 0.05,
|
|
15
|
+
"Combos": 0.05,
|
|
16
|
+
"Finance": 0.04,
|
|
17
|
+
"Politics": 0.04,
|
|
18
|
+
"Mentions": 0.04,
|
|
19
|
+
"Tech": 0.04,
|
|
20
|
+
"Geopolitics": 0.0
|
|
21
|
+
},
|
|
22
|
+
"rounding": {
|
|
23
|
+
"decimals": 5,
|
|
24
|
+
"mode": "half_up",
|
|
25
|
+
"note": "Fees are rounded to 5 decimals (minimum 0.00001 USDC)."
|
|
26
|
+
},
|
|
27
|
+
"maker_rebate_share": {
|
|
28
|
+
"Crypto": 0.2,
|
|
29
|
+
"Sports": 0.15,
|
|
30
|
+
"other fee-enabled categories": 0.25,
|
|
31
|
+
"Combos": 0.0
|
|
32
|
+
},
|
|
33
|
+
"notes": [
|
|
34
|
+
"Some series differ from the category rate (for example NFL and college football markets at 0.03 in November 2026); each market's feeSchedule is authoritative.",
|
|
35
|
+
"Fees apply only to markets created after their category's activation date; older markets can be fee-free."
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"polymarket_us": {
|
|
39
|
+
"as_of": "2026-11-09",
|
|
40
|
+
"name": "Polymarket US (CFTC-regulated app)",
|
|
41
|
+
"source_url": "https://docs.polymarket.us/fees",
|
|
42
|
+
"effective": "2026-11-06 13:00 ET",
|
|
43
|
+
"formula": "fee = Θ × C × p × (1 − p), one curve for all markets",
|
|
44
|
+
"taker_tiers_30d_volume_usd": [
|
|
45
|
+
{
|
|
46
|
+
"below": 10000,
|
|
47
|
+
"theta": 0.095
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"below": 50000,
|
|
51
|
+
"theta": 0.085
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"below": 250000,
|
|
55
|
+
"theta": 0.075
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"below": null,
|
|
59
|
+
"theta": 0.0695
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"rounding": {
|
|
63
|
+
"decimals": 2,
|
|
64
|
+
"mode": "half_even",
|
|
65
|
+
"note": "Rounded to the nearest $0.01 with banker's rounding (half to even)."
|
|
66
|
+
},
|
|
67
|
+
"new_accounts": "Θ = 0.0695 for the first 7 days",
|
|
68
|
+
"makers": "no fee; maker rebates start from $10 million in maker volume",
|
|
69
|
+
"previous": "Θ = 0.0695 for all accounts (0.06 until 2026-09-17)",
|
|
70
|
+
"notes": [
|
|
71
|
+
"Combo taker fees (not modelled in the calculator): until 2026-11-11 13:00 ET fee = C × p × [0.0695 × (1 − p) + 0.085 × (1 − p)^4]; from 2026-11-11 13:00 ET fee = C × p × [Θ × (1 − p) + 0.06 × (1 − p)^4], with Θ the account's taker coefficient. Source: docs.polymarket.us/fees (announced as confirmed, seen 2026-11-09)."
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"kalshi": {
|
|
75
|
+
"as_of": "2026-11-10",
|
|
76
|
+
"name": "Kalshi",
|
|
77
|
+
"source_url": "https://kalshi.com/docs/kalshi-fee-schedule.pdf",
|
|
78
|
+
"source_date": "2026-07-07",
|
|
79
|
+
"version": "7.7.26",
|
|
80
|
+
"formula": "taker fee = round up(M × 0.07 × C × P × (1 − P)), M = 1 by default; maker fee = round up(M × 0.0175 × C × P × (1 − P)), M = 0 by default (only series listed with maker fees). Round up: fee + position cost (C × P) is rounded up to a whole centicent ($0.0001)",
|
|
81
|
+
"taker_rate": 0.07,
|
|
82
|
+
"maker_rate": 0.0175,
|
|
83
|
+
"multipliers": [
|
|
84
|
+
1,
|
|
85
|
+
0.5,
|
|
86
|
+
0
|
|
87
|
+
],
|
|
88
|
+
"rounding": {
|
|
89
|
+
"decimals": 4,
|
|
90
|
+
"mode": "up",
|
|
91
|
+
"basis": "fee_plus_position_cost",
|
|
92
|
+
"source_url": "https://kalshi.com/docs/kalshi-fee-schedule.pdf",
|
|
93
|
+
"version": "7.7.26",
|
|
94
|
+
"note": "Kalshi rounds the fee up so that fee plus position cost (contracts × price) is a whole centicent ($0.0001), per the fee schedule (7.7.26, p. 2). Accounts cleared through a broker (FCM) keep balances in whole cents, so there the fee rounds to the cent.",
|
|
95
|
+
"broker_source_url": "https://docs.kalshi.com/getting_started/fee_rounding.md"
|
|
96
|
+
},
|
|
97
|
+
"notes": [
|
|
98
|
+
"M is a per-series multiplier (1, 0.5 or 0); default 1 for taker fees, 0 for maker fees."
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
pmfees/py.typed
ADDED
|
File without changes
|
pmfees/rates.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Load fee rates from ``prediction_market_fees.json``.
|
|
2
|
+
|
|
3
|
+
The JSON file is the only place rates, tiers, multipliers and rounding rules live.
|
|
4
|
+
Nothing in this package hardcodes a rate: pass ``path=`` (or set ``PMFEES_RATES_FILE``)
|
|
5
|
+
to use an updated file without a new release.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, Decimal
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
ENV_VAR = "PMFEES_RATES_FILE"
|
|
19
|
+
|
|
20
|
+
_ROUNDING_MODES = {"up": ROUND_UP, "half_even": ROUND_HALF_EVEN, "half_up": ROUND_HALF_UP}
|
|
21
|
+
_ROUNDING_WORDS = {
|
|
22
|
+
"up": "rounded up",
|
|
23
|
+
"half_even": "rounded half to even (banker's rounding)",
|
|
24
|
+
"half_up": "rounded half up",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RatesError(ValueError):
|
|
29
|
+
"""The rates file is missing a field or has a value the package cannot use."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
BASES = (None, "fee_plus_position_cost")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Rounding:
|
|
37
|
+
decimals: int
|
|
38
|
+
mode: str
|
|
39
|
+
note: str
|
|
40
|
+
basis: str | None = None
|
|
41
|
+
"""``fee_plus_position_cost`` (Kalshi): fee + position cost (C × P) is rounded, not the fee alone."""
|
|
42
|
+
|
|
43
|
+
def apply(self, value: Decimal, position_cost: Decimal = Decimal(0)) -> Decimal:
|
|
44
|
+
q = Decimal(1).scaleb(-self.decimals)
|
|
45
|
+
if self.basis == "fee_plus_position_cost":
|
|
46
|
+
return (position_cost + value).quantize(q, rounding=_ROUNDING_MODES[self.mode]) - position_cost
|
|
47
|
+
return value.quantize(q, rounding=_ROUNDING_MODES[self.mode])
|
|
48
|
+
|
|
49
|
+
def describe(self) -> str:
|
|
50
|
+
unit = {2: "the cent", 4: "a whole centicent ($0.0001)"}.get(
|
|
51
|
+
self.decimals, f"{self.decimals} decimals"
|
|
52
|
+
)
|
|
53
|
+
if self.basis == "fee_plus_position_cost":
|
|
54
|
+
return f"{_ROUNDING_WORDS[self.mode]} so that fee + position cost is {unit}"
|
|
55
|
+
return f"{_ROUNDING_WORDS[self.mode]} to {unit}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class UsTier:
|
|
60
|
+
number: int
|
|
61
|
+
below: Decimal | None
|
|
62
|
+
theta: Decimal
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def label(self) -> str:
|
|
66
|
+
return f"Tier {self.number}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class PolymarketGlobal:
|
|
71
|
+
name: str
|
|
72
|
+
source_url: str
|
|
73
|
+
formula: str
|
|
74
|
+
rates: dict[str, Decimal]
|
|
75
|
+
rounding: Rounding
|
|
76
|
+
maker_rebate_share: dict[str, Decimal]
|
|
77
|
+
notes: tuple[str, ...]
|
|
78
|
+
as_of: str = ""
|
|
79
|
+
"""Date these rates were last checked against ``source_url``."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class PolymarketUS:
|
|
84
|
+
name: str
|
|
85
|
+
source_url: str
|
|
86
|
+
effective: str
|
|
87
|
+
formula: str
|
|
88
|
+
tiers: tuple[UsTier, ...]
|
|
89
|
+
new_account_theta: Decimal
|
|
90
|
+
new_account_note: str
|
|
91
|
+
makers: str
|
|
92
|
+
rounding: Rounding
|
|
93
|
+
notes: tuple[str, ...]
|
|
94
|
+
as_of: str = ""
|
|
95
|
+
"""Date these rates were last checked against ``source_url``."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Kalshi:
|
|
100
|
+
name: str
|
|
101
|
+
source_url: str
|
|
102
|
+
source_date: str
|
|
103
|
+
formula: str
|
|
104
|
+
taker_rate: Decimal
|
|
105
|
+
maker_rate: Decimal
|
|
106
|
+
multipliers: tuple[Decimal, ...]
|
|
107
|
+
rounding: Rounding
|
|
108
|
+
rounding_source_url: str | None
|
|
109
|
+
notes: tuple[str, ...]
|
|
110
|
+
as_of: str = ""
|
|
111
|
+
"""Date these rates were last checked against ``source_url``."""
|
|
112
|
+
version: str = ""
|
|
113
|
+
"""Version of the Kalshi fee schedule the rates come from (for example ``7.7.26``)."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class Rates:
|
|
118
|
+
as_of: str
|
|
119
|
+
path: str
|
|
120
|
+
polymarket_global: PolymarketGlobal
|
|
121
|
+
polymarket_us: PolymarketUS
|
|
122
|
+
kalshi: Kalshi
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get(obj: dict[str, Any], key: str, where: str) -> Any:
|
|
126
|
+
if key not in obj:
|
|
127
|
+
raise RatesError(f"{where}: missing field {key!r}")
|
|
128
|
+
return obj[key]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _dec(value: Any, where: str) -> Decimal:
|
|
132
|
+
if isinstance(value, bool) or not isinstance(value, (Decimal, int)):
|
|
133
|
+
raise RatesError(f"{where}: expected a number, got {value!r}")
|
|
134
|
+
d = Decimal(value)
|
|
135
|
+
if d < 0:
|
|
136
|
+
raise RatesError(f"{where}: negative value {value!r}")
|
|
137
|
+
return d
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _rounding(obj: dict[str, Any], where: str) -> Rounding:
|
|
141
|
+
mode = _get(obj, "mode", where)
|
|
142
|
+
if mode not in _ROUNDING_MODES:
|
|
143
|
+
raise RatesError(f"{where}: unknown rounding mode {mode!r}")
|
|
144
|
+
decimals = _get(obj, "decimals", where)
|
|
145
|
+
if not isinstance(decimals, int) or decimals < 0:
|
|
146
|
+
raise RatesError(f"{where}: decimals must be a non-negative integer")
|
|
147
|
+
basis = obj.get("basis")
|
|
148
|
+
if basis not in BASES:
|
|
149
|
+
raise RatesError(f"{where}: unknown rounding basis {basis!r}")
|
|
150
|
+
return Rounding(decimals=decimals, mode=mode, note=str(obj.get("note", "")), basis=basis)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _new_account_theta(text: str, where: str) -> Decimal:
|
|
154
|
+
# The file states it as text, e.g. "Θ = 0.0695 for the first 7 days".
|
|
155
|
+
for token in text.replace("=", " ").split():
|
|
156
|
+
try:
|
|
157
|
+
return _dec(Decimal(token), where)
|
|
158
|
+
except ArithmeticError:
|
|
159
|
+
continue
|
|
160
|
+
raise RatesError(f"{where}: no coefficient found in {text!r}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_rates(data: dict[str, Any], path: str = "<memory>") -> Rates:
|
|
164
|
+
"""Build :class:`Rates` from the decoded JSON (numbers decoded as ``Decimal``)."""
|
|
165
|
+
g = _get(data, "polymarket_global", "root")
|
|
166
|
+
us = _get(data, "polymarket_us", "root")
|
|
167
|
+
k = _get(data, "kalshi", "root")
|
|
168
|
+
|
|
169
|
+
tiers: list[UsTier] = []
|
|
170
|
+
raw_tiers = _get(us, "taker_tiers_30d_volume_usd", "polymarket_us")
|
|
171
|
+
for i, t in enumerate(raw_tiers, start=1):
|
|
172
|
+
where = f"polymarket_us.taker_tiers_30d_volume_usd[{i - 1}]"
|
|
173
|
+
below = _get(t, "below", where)
|
|
174
|
+
tiers.append(
|
|
175
|
+
UsTier(
|
|
176
|
+
number=i,
|
|
177
|
+
below=None if below is None else _dec(below, where),
|
|
178
|
+
theta=_dec(_get(t, "theta", where), where),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
if not tiers or tiers[-1].below is not None:
|
|
182
|
+
raise RatesError("polymarket_us: the last tier must have below = null")
|
|
183
|
+
|
|
184
|
+
new_accounts = str(_get(us, "new_accounts", "polymarket_us"))
|
|
185
|
+
rounding_k = _get(k, "rounding", "kalshi")
|
|
186
|
+
as_of = str(_get(data, "as_of", "root"))
|
|
187
|
+
|
|
188
|
+
return Rates(
|
|
189
|
+
as_of=as_of,
|
|
190
|
+
path=path,
|
|
191
|
+
polymarket_global=PolymarketGlobal(
|
|
192
|
+
name=str(_get(g, "name", "polymarket_global")),
|
|
193
|
+
source_url=str(_get(g, "source_url", "polymarket_global")),
|
|
194
|
+
formula=str(_get(g, "formula", "polymarket_global")),
|
|
195
|
+
rates={
|
|
196
|
+
cat: _dec(r, f"polymarket_global.rates.{cat}")
|
|
197
|
+
for cat, r in _get(g, "rates", "polymarket_global").items()
|
|
198
|
+
},
|
|
199
|
+
rounding=_rounding(_get(g, "rounding", "polymarket_global"), "polymarket_global.rounding"),
|
|
200
|
+
maker_rebate_share={
|
|
201
|
+
cat: _dec(r, f"polymarket_global.maker_rebate_share.{cat}")
|
|
202
|
+
for cat, r in g.get("maker_rebate_share", {}).items()
|
|
203
|
+
},
|
|
204
|
+
notes=tuple(g.get("notes", ())),
|
|
205
|
+
as_of=str(g.get("as_of") or as_of),
|
|
206
|
+
),
|
|
207
|
+
polymarket_us=PolymarketUS(
|
|
208
|
+
name=str(_get(us, "name", "polymarket_us")),
|
|
209
|
+
source_url=str(_get(us, "source_url", "polymarket_us")),
|
|
210
|
+
effective=str(us.get("effective", "")),
|
|
211
|
+
formula=str(_get(us, "formula", "polymarket_us")),
|
|
212
|
+
tiers=tuple(tiers),
|
|
213
|
+
new_account_theta=_new_account_theta(new_accounts, "polymarket_us.new_accounts"),
|
|
214
|
+
new_account_note=new_accounts,
|
|
215
|
+
makers=str(us.get("makers", "")),
|
|
216
|
+
rounding=_rounding(_get(us, "rounding", "polymarket_us"), "polymarket_us.rounding"),
|
|
217
|
+
notes=tuple(us.get("notes", ())),
|
|
218
|
+
as_of=str(us.get("as_of") or as_of),
|
|
219
|
+
),
|
|
220
|
+
kalshi=Kalshi(
|
|
221
|
+
name=str(_get(k, "name", "kalshi")),
|
|
222
|
+
source_url=str(_get(k, "source_url", "kalshi")),
|
|
223
|
+
source_date=str(k.get("source_date", "")),
|
|
224
|
+
formula=str(_get(k, "formula", "kalshi")),
|
|
225
|
+
taker_rate=_dec(_get(k, "taker_rate", "kalshi"), "kalshi.taker_rate"),
|
|
226
|
+
maker_rate=_dec(_get(k, "maker_rate", "kalshi"), "kalshi.maker_rate"),
|
|
227
|
+
multipliers=tuple(_dec(m, "kalshi.multipliers") for m in _get(k, "multipliers", "kalshi")),
|
|
228
|
+
rounding=_rounding(rounding_k, "kalshi.rounding"),
|
|
229
|
+
rounding_source_url=rounding_k.get("source_url"),
|
|
230
|
+
notes=tuple(k.get("notes", ())),
|
|
231
|
+
as_of=str(k.get("as_of") or as_of),
|
|
232
|
+
version=str(k.get("version", "")),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def load_rates(path: str | os.PathLike[str] | None = None) -> Rates:
|
|
238
|
+
"""Load rates from ``path``, else ``$PMFEES_RATES_FILE``, else the bundled file."""
|
|
239
|
+
chosen = path if path is not None else os.environ.get(ENV_VAR) or None
|
|
240
|
+
if chosen is not None:
|
|
241
|
+
p = Path(chosen)
|
|
242
|
+
text = p.read_text(encoding="utf-8")
|
|
243
|
+
label = str(p)
|
|
244
|
+
else:
|
|
245
|
+
res = resources.files("pmfees").joinpath("data/prediction_market_fees.json")
|
|
246
|
+
text = res.read_text(encoding="utf-8")
|
|
247
|
+
label = "bundled prediction_market_fees.json"
|
|
248
|
+
try:
|
|
249
|
+
data = json.loads(text, parse_float=Decimal)
|
|
250
|
+
except json.JSONDecodeError as exc:
|
|
251
|
+
raise RatesError(f"{label}: not valid JSON ({exc})") from exc
|
|
252
|
+
if not isinstance(data, dict):
|
|
253
|
+
raise RatesError(f"{label}: expected a JSON object")
|
|
254
|
+
return parse_rates(data, label)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
_default: Rates | None = None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def default_rates() -> Rates:
|
|
261
|
+
"""The rates used when no ``rates=`` argument is given (loaded once)."""
|
|
262
|
+
global _default
|
|
263
|
+
if _default is None:
|
|
264
|
+
_default = load_rates()
|
|
265
|
+
return _default
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmfees
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact trading fees for Kalshi, Polymarket US and polymarket.com, with a CLI and break-even helper.
|
|
5
|
+
Author-email: SwapHunt <github@swaphunt.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/swaphunt/pmfees
|
|
8
|
+
Project-URL: Source, https://github.com/swaphunt/pmfees
|
|
9
|
+
Project-URL: Issues, https://github.com/swaphunt/pmfees/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/swaphunt/pmfees/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Fee calculator, https://swaphunt.dev/tools/kalshi-vs-polymarket-fee-calculator
|
|
12
|
+
Keywords: kalshi,polymarket,prediction markets,fees,trading
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pmfees
|
|
24
|
+
|
|
25
|
+
Exact trading fees for the prediction market platforms **Kalshi**, **Polymarket US** and
|
|
26
|
+
**polymarket.com** (international), as a small Python library and a command line tool.
|
|
27
|
+
|
|
28
|
+
- The platforms' own formulas and rounding rules: Kalshi rounds up so that fee + position cost
|
|
29
|
+
is a whole centicent ($0.0001), Polymarket US rounds to the cent with banker's rounding,
|
|
30
|
+
polymarket.com rounds to 5 decimals.
|
|
31
|
+
- Exact decimal arithmetic (`decimal.Decimal`), so $1.75 is $1.75 and not $1.7500000000000002.
|
|
32
|
+
- A break-even helper: how far the price has to move before a round trip is in profit after
|
|
33
|
+
the fee on both orders.
|
|
34
|
+
- No dependencies outside the Python standard library. Typed (`py.typed`).
|
|
35
|
+
|
|
36
|
+
Rates current as of: Kalshi 2026-11-10 (fee schedule 7.7.26), Polymarket US 2026-11-09,
|
|
37
|
+
polymarket.com 2026-11-08. Each platform's date and source are in the rates file and in every result.
|
|
38
|
+
|
|
39
|
+
Other ways to get the same numbers, from the same rates file:
|
|
40
|
+
|
|
41
|
+
- In a browser: the [SwapHunt Kalshi vs Polymarket fee calculator](https://swaphunt.dev/tools/kalshi-vs-polymarket-fee-calculator).
|
|
42
|
+
- Over HTTP, for agents: [`GET https://x402.swaphunt.dev/v1/pm/fees`](https://swaphunt.dev/api), paid per call
|
|
43
|
+
with [x402](https://www.x402.org/) in USDC. This package is free; the endpoint exists for agents that
|
|
44
|
+
cannot install a library.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pmfees
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python 3.10 or newer.
|
|
53
|
+
|
|
54
|
+
## Command line
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pmfees kalshi --price 0.50 --contracts 100
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
Kalshi taker fee: $1.75 (3.50% of trade value at 50¢)
|
|
62
|
+
buy 100 contracts at $0.50 = $50.00 trade value
|
|
63
|
+
base rate: 0.07
|
|
64
|
+
multiplier: 1
|
|
65
|
+
fee schedule version: 7.7.26
|
|
66
|
+
0.07 × 100 × 0.5 × (1 − 0.5) = 1.75, rounded up so that fee + position cost is a whole centicent ($0.0001): $1.75
|
|
67
|
+
rates as of 2026-11-10 · source: https://kalshi.com/docs/kalshi-fee-schedule.pdf
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
More examples:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Polymarket US, Tier 1 (under $10,000 of 30-day taker volume, the default)
|
|
74
|
+
pmfees polymarket-us --price 0.50 --contracts 100
|
|
75
|
+
# -> Polymarket US taker fee: $2.38 (4.76% of trade value at 50¢) [0.095 × 100 × 0.5 × 0.5 = 2.375]
|
|
76
|
+
|
|
77
|
+
# polymarket.com, Crypto category
|
|
78
|
+
pmfees polymarket --price 0.25 --contracts 100 --category Crypto
|
|
79
|
+
# -> polymarket.com taker fee: $1.3125 (5.25% of trade value at 25¢)
|
|
80
|
+
|
|
81
|
+
# Break-even: buy 100 on Kalshi at 50¢, when is selling in profit?
|
|
82
|
+
pmfees kalshi --price 0.50 --contracts 100 --break-even
|
|
83
|
+
# -> Break-even: the price must rise 4¢ to 54¢ (tick 1¢); entry fee $1.75 + exit fee $1.7388, profit at that price $0.5112
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
|
|
88
|
+
| Option | Platforms | Meaning |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `--price` | all | Price of the contract you trade, in dollars (0 < price < 1) |
|
|
91
|
+
| `--contracts` | all | Number of contracts (shares on polymarket.com) |
|
|
92
|
+
| `--side buy\|sell` | all | Changes the cash flow only; the fee formulas are symmetric |
|
|
93
|
+
| `--maker` | all | Maker order. Kalshi: maker rate 0.0175 with M default 0, so pass `--multiplier 1` on markets with maker fees. Polymarket US and polymarket.com: makers pay no fee |
|
|
94
|
+
| `--multiplier` | kalshi | Fee multiplier M (default 1 for takers, 0 for makers) |
|
|
95
|
+
| `--tier 1-4\|new` | polymarket-us | Taker tier, or `new` for the first 7 days of a new account (default 1) |
|
|
96
|
+
| `--volume` | polymarket-us | Trailing 30-day taker volume in USD; picks the tier for you |
|
|
97
|
+
| `--category` | polymarket | Market category, e.g. `Crypto`, `Sports`, `Politics` (required) |
|
|
98
|
+
| `--break-even`, `--tick` | all | Also print the break-even move (price step, default 0.01) |
|
|
99
|
+
| `--json` | all | Machine-readable output |
|
|
100
|
+
| `--rates PATH` | all | Use an updated rates file |
|
|
101
|
+
|
|
102
|
+
`pmfees rates` prints the loaded rates and their date.
|
|
103
|
+
|
|
104
|
+
## Library
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from pmfees import fee, break_even
|
|
108
|
+
|
|
109
|
+
r = fee("kalshi", 0.50, 100)
|
|
110
|
+
r.fee # Decimal('1.7500') what you pay, in USD
|
|
111
|
+
r.raw_fee # Decimal('1.7500') before rounding
|
|
112
|
+
r.fee_pct_of_trade_value # Decimal('3.500')
|
|
113
|
+
r.breakdown() # dict of strings: coefficient, rounding, formula, source_url, rates_as_of, ...
|
|
114
|
+
|
|
115
|
+
fee("polymarket-us", 0.10, 1000, "buy", tier=4).fee # Decimal('6.26')
|
|
116
|
+
fee("polymarket-us", 0.10, 1000, volume_30d=12_500).fee # Tier 2
|
|
117
|
+
fee("polymarket", 0.25, 100, category="Crypto").fee # Decimal('1.31250')
|
|
118
|
+
fee("kalshi", 0.50, 1).fee # Decimal('0.0175') centicent rounding, not $0.02
|
|
119
|
+
fee("kalshi", 0.50, 100, maker=True).fee # Decimal('0.0000') maker M defaults to 0
|
|
120
|
+
fee("kalshi", 0.50, 100, maker=True, multiplier=1).fee # Decimal('0.4375')
|
|
121
|
+
fee("kalshi", 0.50, 100, multiplier=0.5).fee # Decimal('0.8750')
|
|
122
|
+
|
|
123
|
+
b = break_even("polymarket-us", 0.40, 500, tier=2)
|
|
124
|
+
b.exit_price, b.move_cents # (Decimal('0.45'), Decimal('5.00'))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Signature: `fee(platform, price, contracts, side="buy", tier=None, maker=False, *, category=None,
|
|
128
|
+
multiplier=None, volume_30d=None, rates=None) -> FeeResult`. Platform names: `kalshi`,
|
|
129
|
+
`polymarket-us`, `polymarket` (also `polymarket.com`). Prices and counts can be `str`, `int`,
|
|
130
|
+
`float` or `Decimal`; floats are converted through `str`, so `0.1` means exactly 0.1.
|
|
131
|
+
|
|
132
|
+
`break_even(...)` takes the same arguments plus `tick`. It opens the position at `price`, then
|
|
133
|
+
walks the price in `tick` steps (up for a buy, down for a sell) and returns the first exit price
|
|
134
|
+
where proceeds − cost − entry fee − exit fee > 0, or `exit_price=None` if no price before $1 (or
|
|
135
|
+
$0) works.
|
|
136
|
+
|
|
137
|
+
## Kalshi: centicent rounding and the multiplier
|
|
138
|
+
|
|
139
|
+
Kalshi does not round the fee on its own. It rounds **fee + position cost** (contracts × price) up to
|
|
140
|
+
a whole centicent ($0.0001), per the fee schedule (version 7.7.26, page 2). Most of the time that
|
|
141
|
+
looks like rounding the fee up to four decimals, but the fee alone is what you see change:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from pmfees import fee
|
|
145
|
+
|
|
146
|
+
fee("kalshi", 0.50, 1).fee # Decimal('0.0175') 0.07 × 1 × 0.5 × 0.5 = 0.0175 exactly; not $0.02
|
|
147
|
+
fee("kalshi", 0.33, 7).fee # Decimal('0.1084') raw 0.108339; 2.31 + 0.108339 = 2.418339 -> 2.4184
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Accounts cleared through a broker (FCM) keep balances in whole cents, so there the fee rounds to the
|
|
151
|
+
cent ([Kalshi docs](https://docs.kalshi.com/getting_started/fee_rounding.md)). pmfees models
|
|
152
|
+
kalshi.com accounts (direct members).
|
|
153
|
+
|
|
154
|
+
Each Kalshi series has a fee multiplier M: 1 (standard), 0.5 or 0. Takers default to M = 1, makers
|
|
155
|
+
to M = 0, because Kalshi charges makers only on series listed with maker fees:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
fee("kalshi", 0.25, 100).fee # Decimal('1.3125') M = 1
|
|
159
|
+
fee("kalshi", 0.25, 100, multiplier=0.5).fee # Decimal('0.6563') raw 0.65625, rounded up
|
|
160
|
+
fee("kalshi", 0.50, 100, maker=True).fee # Decimal('0.0000') maker, M defaults to 0
|
|
161
|
+
fee("kalshi", 0.50, 100, maker=True, multiplier=1).fee # Decimal('0.4375') 0.0175 × 100 × 0.25
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pmfees kalshi --price 0.25 --contracts 100 --multiplier 0.5
|
|
166
|
+
# -> Kalshi taker fee: $0.6563 (2.63% of trade value at 25¢)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## The formulas
|
|
170
|
+
|
|
171
|
+
| Platform | Taker fee | Rounding |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| Kalshi | M × 0.07 × C × P × (1 − P), M default 1; maker M × 0.0175 × C × P × (1 − P), M default 0 | Up, so that fee + position cost is a whole centicent ($0.0001) |
|
|
174
|
+
| Polymarket US | Θ × C × p × (1 − p), Θ by 30-day taker volume: 0.095 (< $10K), 0.085 (< $50K), 0.075 (< $250K), 0.0695 ($250K+); new accounts 0.0695 for the first 7 days | Nearest cent, half to even |
|
|
175
|
+
| polymarket.com | C × rate × p × (1 − p), rate by category (Crypto 0.07; Sports, Economics, Culture, Weather, Other, Combos 0.05; Finance, Politics, Mentions, Tech 0.04; Geopolitics 0) | 5 decimals |
|
|
176
|
+
|
|
177
|
+
The numbers in this table are for reading only. The code takes every rate, tier, multiplier and
|
|
178
|
+
rounding rule (including Kalshi's fee + position cost basis) from `prediction_market_fees.json`.
|
|
179
|
+
|
|
180
|
+
Not modelled: Polymarket US combo fees and maker rebates, polymarket.com maker and taker rebate
|
|
181
|
+
programs, and per-market exceptions (polymarket.com
|
|
182
|
+
markets carry their own `feeSchedule`, Kalshi series their own multiplier). Check the market when
|
|
183
|
+
it matters.
|
|
184
|
+
|
|
185
|
+
## Sources
|
|
186
|
+
|
|
187
|
+
| Platform | Source | Rates current as of |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| Kalshi | <https://kalshi.com/docs/kalshi-fee-schedule.pdf> (version 7.7.26) | 2026-11-10 |
|
|
190
|
+
| Polymarket US | <https://docs.polymarket.us/fees> | 2026-11-09 |
|
|
191
|
+
| polymarket.com | <https://docs.polymarket.com/trading/fees> | 2026-11-08 |
|
|
192
|
+
|
|
193
|
+
The tests lock the formulas to the worked examples and fee tables on these pages, with a link
|
|
194
|
+
to the source in each test.
|
|
195
|
+
|
|
196
|
+
## Updating the rates
|
|
197
|
+
|
|
198
|
+
All rates live in `src/pmfees/data/prediction_market_fees.json`. Each platform has its own `as_of`
|
|
199
|
+
(the date its rates were last checked against `source_url`). SwapHunt checks the official fee pages
|
|
200
|
+
every week; when a rate changes, the updated file arrives here as a pull request, and a new release
|
|
201
|
+
follows after review. To use newer rates without waiting for a release:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from pmfees import fee, load_rates
|
|
205
|
+
|
|
206
|
+
rates = load_rates("prediction_market_fees.json")
|
|
207
|
+
fee("kalshi", 0.50, 100, rates=rates)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
or set `PMFEES_RATES_FILE=/path/to/prediction_market_fees.json`, or pass `--rates` on the
|
|
211
|
+
command line. A file with a missing field or an unknown rounding mode is rejected with a
|
|
212
|
+
`RatesError` that names the field.
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pip install -e . pytest ruff mypy
|
|
218
|
+
pytest
|
|
219
|
+
ruff check . && ruff format --check .
|
|
220
|
+
mypy
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Releases go to PyPI from GitHub Actions with trusted publishing: bump `__version__`, add a
|
|
224
|
+
CHANGELOG entry, and push a `v<version>` tag. The publish job waits for approval.
|
|
225
|
+
|
|
226
|
+
## Disclaimer
|
|
227
|
+
|
|
228
|
+
Fee estimates for information only. The platform's own confirmation is what you pay.
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pmfees/__init__.py,sha256=I897uC5YPdAsjPeH91a9eFlZkkh7DwQznW9Z2zicNrE,659
|
|
2
|
+
pmfees/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
pmfees/cli.py,sha256=TkAhS6KvRm7NKbyZ0n4Zg2_S4ylKSiPqfWH7m6Chi5A,7482
|
|
4
|
+
pmfees/core.py,sha256=uUPWjXAeg7_m5Wv_Haug-a31udtAnBL1RpWf2A7k9Xs,13258
|
|
5
|
+
pmfees/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pmfees/rates.py,sha256=FuaPKmqN8JUIMxnG0bTvQVHCG0ZvBIxwV7zN-l4zv0M,9338
|
|
7
|
+
pmfees/data/prediction_market_fees.json,sha256=4cGczSPTImETD_2pF7RUSFifEr1azZ4YF327wmgq8lE,3757
|
|
8
|
+
pmfees-0.1.0.dist-info/licenses/LICENSE,sha256=T-Ys1tHHacpFh5TuhSsZ_4qsGuZasUG5I_RMUG6PK-k,1065
|
|
9
|
+
pmfees-0.1.0.dist-info/METADATA,sha256=dfvX67_NII5EJtoG4X9dbtizDxWAedgRs7pcUpgNoy4,10463
|
|
10
|
+
pmfees-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
11
|
+
pmfees-0.1.0.dist-info/entry_points.txt,sha256=sugxa34guA5WlSdLmAXmXaY_AtAm73i0tskaSLmI5DM,43
|
|
12
|
+
pmfees-0.1.0.dist-info/top_level.txt,sha256=f8789C1LmCwDMPY8onCj6bSk0oHTIpLr6q-1Iyh7UKw,7
|
|
13
|
+
pmfees-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SwapHunt
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pmfees
|