Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"Bash(uv run:*)",
"Bash(uv add:*)",
"Bash(uv sync:*)",
"Bash(find:*)"
"Bash(find:*)",
"WebFetch(domain:docs.kalshi.com)"
],
"deny": [],
"ask": []
}
}
}
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
3. Only use UV to install dependencies and run the python application.
4. Single Source of Truth: DO NOT place many variables in .env file. Place them in the code instead.
5. Run and Debug yourself PROACTIVELY.
6. When integrating a new exchange, create a wiki documentation file at `wiki/exchanges/<exchange_name>.md` using the template at `wiki/exchanges/TEMPLATE.md`.
3 changes: 2 additions & 1 deletion dr_manhattan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .exchanges.polymarket import Polymarket
from .exchanges.predictfun import PredictFun
from .models.market import ExchangeOutcomeRef, Market, OutcomeRef, ReadableMarketId
from .models.order import Order, OrderSide, OrderStatus
from .models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from .models.position import Position

__version__ = "0.0.1"
Expand All @@ -57,6 +57,7 @@
"Order",
"OrderSide",
"OrderStatus",
"OrderTimeInForce",
"Position",
"Polymarket",
"Limitless",
Expand Down
7 changes: 6 additions & 1 deletion dr_manhattan/base/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..base.errors import NetworkError, RateLimitError
from ..models.crypto_hourly import CryptoHourlyMarket
from ..models.market import Market
from ..models.order import Order, OrderSide
from ..models.order import Order, OrderSide, OrderTimeInForce
from ..models.position import Position


Expand Down Expand Up @@ -109,6 +109,7 @@ def create_order(
price: float,
size: float,
params: Optional[Dict[str, Any]] = None,
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC,
) -> Order:
"""
Create a new order.
Expand All @@ -120,6 +121,10 @@ def create_order(
price: Price per share (0-1 or 0-100 depending on exchange)
size: Number of shares
params: Additional exchange-specific parameters
time_in_force: Order time in force (GTC, FOK, IOC). Default is GTC.
- GTC (Good-Til-Cancel): Order remains active until filled or cancelled
- FOK (Fill-Or-Kill): Order must be filled immediately and completely or cancelled
- IOC (Immediate-Or-Cancel): Fill what's available immediately, cancel the rest

Returns:
Order object
Expand Down
26 changes: 25 additions & 1 deletion dr_manhattan/exchanges/kalshi.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)
from ..base.exchange import Exchange
from ..models.market import Market
from ..models.order import Order, OrderSide, OrderStatus
from ..models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from ..models.orderbook import Orderbook
from ..models.position import Position

Expand Down Expand Up @@ -469,7 +469,23 @@ def create_order(
price: float,
size: float,
params: Optional[Dict[str, Any]] = None,
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC,
) -> Order:
"""
Create a new order on Kalshi.

Args:
market_id: Market ticker
outcome: Outcome to bet on ("Yes" or "No")
side: OrderSide.BUY or OrderSide.SELL
price: Price per contract (0-1)
size: Number of contracts
params: Additional parameters
time_in_force: Order time in force. Kalshi supports GTC, FOK, and IOC.

Returns:
Order object
"""
self._ensure_auth()

if price <= 0 or price >= 1:
Expand All @@ -482,12 +498,20 @@ def create_order(
action = "buy" if side == OrderSide.BUY else "sell"
price_cents = int(round(price * 100))

# Map time_in_force to Kalshi API values
tif_map = {
OrderTimeInForce.GTC: "good_till_canceled",
OrderTimeInForce.FOK: "fill_or_kill",
OrderTimeInForce.IOC: "immediate_or_cancel",
}

body: Dict[str, Any] = {
"ticker": market_id,
"action": action,
"side": outcome_lower,
"type": "limit",
"count": int(size),
"time_in_force": tif_map.get(time_in_force, "good_till_canceled"),
}

if outcome_lower == "yes":
Expand Down
15 changes: 13 additions & 2 deletions dr_manhattan/exchanges/limitless.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from ..base.exchange import Exchange
from ..models.market import Market
from ..models.order import Order, OrderSide, OrderStatus
from ..models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from ..models.position import Position
from .limitless_ws import (
LimitlessUserWebSocket,
Expand Down Expand Up @@ -636,6 +636,7 @@ def create_order(
price: float,
size: float,
params: Optional[Dict[str, Any]] = None,
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC,
) -> Order:
"""
Create a new order on Limitless.
Expand All @@ -649,12 +650,18 @@ def create_order(
params: Additional parameters:
- token_id: Token ID (optional if outcome provided)
- order_type: "GTC" or "FOK" (default: "GTC")
time_in_force: Order time in force. Limitless supports GTC and FOK only.
IOC is not supported and will raise InvalidOrder.

Returns:
Order object
"""
self._ensure_authenticated()

# Validate time_in_force - Limitless only supports GTC and FOK
if time_in_force == OrderTimeInForce.IOC:
raise InvalidOrder("Limitless does not support IOC orders. Use GTC or FOK instead.")

extra_params = params or {}
token_id = extra_params.get("token_id")

Expand All @@ -670,7 +677,11 @@ def create_order(
if price <= 0 or price >= 1:
raise InvalidOrder(f"Price must be between 0 and 1, got: {price}")

order_type = extra_params.get("order_type", "GTC").upper()
# Map time_in_force to order_type, or use params override
order_type = extra_params.get("order_type")
if not order_type:
order_type = "FOK" if time_in_force == OrderTimeInForce.FOK else "GTC"
order_type = order_type.upper()

# Get venue exchange address for EIP-712 signing
venue = market.metadata.get("venue", {})
Expand Down
13 changes: 12 additions & 1 deletion dr_manhattan/exchanges/opinion.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from ..base.exchange import Exchange
from ..models.market import Market
from ..models.order import Order, OrderSide, OrderStatus
from ..models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from ..models.position import Position


Expand Down Expand Up @@ -566,6 +566,7 @@ def create_order(
price: float,
size: float,
params: Optional[Dict[str, Any]] = None,
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC,
) -> Order:
"""
Create a new order on Opinion.
Expand All @@ -580,12 +581,21 @@ def create_order(
- token_id: Token ID (required)
- order_type: "limit" or "market" (default: "limit")
- check_approval: Whether to check approvals (default: False)
time_in_force: Order time in force. Opinion only supports GTC.
FOK and IOC are not supported and will raise InvalidOrder.

Returns:
Order object
"""
self._ensure_client()

# Validate time_in_force - Opinion only supports GTC
if time_in_force != OrderTimeInForce.GTC:
raise InvalidOrder(
f"Opinion does not support {time_in_force.value.upper()} orders. "
"Only GTC orders are supported."
)

extra_params = params or {}
token_id = extra_params.get("token_id")

Expand Down Expand Up @@ -643,6 +653,7 @@ def create_order(
status=status,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
time_in_force=time_in_force,
)

except InvalidOrder:
Expand Down
14 changes: 12 additions & 2 deletions dr_manhattan/exchanges/polymarket.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..base.exchange import Exchange
from ..models import CryptoHourlyMarket
from ..models.market import Market
from ..models.order import Order, OrderSide, OrderStatus
from ..models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from ..models.position import Position
from ..utils import setup_logger
from .polymarket_ws import PolymarketUserWebSocket, PolymarketWebSocket
Expand Down Expand Up @@ -781,6 +781,7 @@ def create_order(
price: float,
size: float,
params: Optional[Dict[str, Any]] = None,
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC,
) -> Order:
"""Create order on Polymarket CLOB"""
if not self._clob_client:
Expand All @@ -790,6 +791,14 @@ def create_order(
if not token_id:
raise InvalidOrder("token_id required in params")

# Map our OrderTimeInForce to py_clob_client OrderType
order_type_map = {
OrderTimeInForce.GTC: OrderType.GTC,
OrderTimeInForce.FOK: OrderType.FOK,
OrderTimeInForce.IOC: OrderType.GTD, # py_clob_client uses GTD for IOC behavior
}
clob_order_type = order_type_map.get(time_in_force, OrderType.GTC)

try:
# Create and sign order
order_args = OrderArgs(
Expand All @@ -800,7 +809,7 @@ def create_order(
)

signed_order = self._clob_client.create_order(order_args)
result = self._clob_client.post_order(signed_order, OrderType.GTC)
result = self._clob_client.post_order(signed_order, clob_order_type)

# Parse result
order_id = result.get("orderID", "") if isinstance(result, dict) else str(result)
Expand All @@ -823,6 +832,7 @@ def create_order(
status=status_map.get(status_str, OrderStatus.OPEN),
created_at=datetime.now(),
updated_at=datetime.now(),
time_in_force=time_in_force,
)

except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion dr_manhattan/exchanges/predictfun.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class PredictFun(Exchange):

@property
def id(self) -> str:
return "predictfun"
return "predict.fun"

@property
def name(self) -> str:
Expand Down
3 changes: 2 additions & 1 deletion dr_manhattan/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .crypto_hourly import CryptoHourlyMarket
from .market import ExchangeOutcomeRef, Market, OutcomeRef, OutcomeToken
from .nav import NAV, PositionBreakdown
from .order import Order, OrderSide, OrderStatus
from .order import Order, OrderSide, OrderStatus, OrderTimeInForce
from .orderbook import Orderbook, PriceLevel
from .position import Position

Expand All @@ -13,6 +13,7 @@
"Order",
"OrderSide",
"OrderStatus",
"OrderTimeInForce",
"Orderbook",
"PriceLevel",
"Position",
Expand Down
7 changes: 7 additions & 0 deletions dr_manhattan/models/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class OrderStatus(Enum):
REJECTED = "rejected"


class OrderTimeInForce(Enum):
GTC = "gtc" # Good-Til-Cancel: remains active until filled or cancelled
FOK = "fok" # Fill-Or-Kill: must be filled immediately and completely or cancelled
IOC = "ioc" # Immediate-Or-Cancel: fill what's available immediately, cancel rest


@dataclass
class Order:
"""Represents an order on a prediction market"""
Expand All @@ -32,6 +38,7 @@ class Order:
status: OrderStatus
created_at: datetime
updated_at: Optional[datetime] = None
time_in_force: OrderTimeInForce = OrderTimeInForce.GTC

@property
def remaining(self) -> float:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ def test_iterate_all_exchanges(self):
exchange = dr_manhattan.exchanges[exchange_id]()
exchanges.append(exchange)

assert len(exchanges) == 4
assert len(exchanges) == 5
assert all(isinstance(e, Exchange) for e in exchanges)

def test_exchange_count(self):
"""Test number of registered exchanges"""
assert len(dr_manhattan.exchanges) == 4
assert len(dr_manhattan.exchanges) == 5
54 changes: 53 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime

from dr_manhattan.models.market import Market
from dr_manhattan.models.order import Order, OrderSide, OrderStatus
from dr_manhattan.models.order import Order, OrderSide, OrderStatus, OrderTimeInForce
from dr_manhattan.models.position import Position


Expand Down Expand Up @@ -357,3 +357,55 @@ def test_order_status_enum(self):
assert OrderStatus.PARTIALLY_FILLED.value == "partially_filled"
assert OrderStatus.CANCELLED.value == "cancelled"
assert OrderStatus.REJECTED.value == "rejected"

def test_order_time_in_force_enum(self):
"""Test OrderTimeInForce enum"""
assert OrderTimeInForce.GTC.value == "gtc"
assert OrderTimeInForce.FOK.value == "fok"
assert OrderTimeInForce.IOC.value == "ioc"

def test_order_with_time_in_force(self):
"""Test creating an order with time_in_force"""
# Test default (GTC)
order_default = Order(
id="o1",
market_id="m1",
outcome="Yes",
side=OrderSide.BUY,
price=0.65,
size=100,
filled=0,
status=OrderStatus.OPEN,
created_at=datetime(2025, 1, 1),
)
assert order_default.time_in_force == OrderTimeInForce.GTC

# Test FOK
order_fok = Order(
id="o2",
market_id="m1",
outcome="Yes",
side=OrderSide.BUY,
price=0.65,
size=100,
filled=0,
status=OrderStatus.OPEN,
created_at=datetime(2025, 1, 1),
time_in_force=OrderTimeInForce.FOK,
)
assert order_fok.time_in_force == OrderTimeInForce.FOK

# Test IOC
order_ioc = Order(
id="o3",
market_id="m1",
outcome="Yes",
side=OrderSide.BUY,
price=0.65,
size=100,
filled=0,
status=OrderStatus.OPEN,
created_at=datetime(2025, 1, 1),
time_in_force=OrderTimeInForce.IOC,
)
assert order_ioc.time_in_force == OrderTimeInForce.IOC
Loading