|
@@ -12,6 +12,9 @@ from .bitstamp_metadata import load_market_by_symbol
|
|
|
from .storage import get_connection
|
|
from .storage import get_connection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+OPEN_ORDER_STATUSES = {"open", "new", "partially_filled"}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _utc_now() -> str:
|
|
def _utc_now() -> str:
|
|
|
return datetime.now(timezone.utc).isoformat()
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
@@ -32,6 +35,30 @@ def _format_decimal(value, decimals: int) -> str:
|
|
|
return str(Decimal(str(value)).quantize(quant, rounding=ROUND_DOWN))
|
|
return str(Decimal(str(value)).quantize(quant, rounding=ROUND_DOWN))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _normalize_client_id(client_id: str | None) -> str | None:
|
|
|
|
|
+ if client_id is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ if not isinstance(client_id, str):
|
|
|
|
|
+ raise HTTPException(status_code=400, detail="client_id must be a string")
|
|
|
|
|
+ client_id = client_id.strip()
|
|
|
|
|
+ if not client_id:
|
|
|
|
|
+ raise HTTPException(status_code=400, detail="client_id must not be empty")
|
|
|
|
|
+ return client_id
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _normalize_status(status) -> str:
|
|
|
|
|
+ return str(status or "unknown").strip().lower().replace(" ", "_")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _set_local_order_status(*, bitstamp_order_id: str, status: str) -> None:
|
|
|
|
|
+ with get_connection() as conn:
|
|
|
|
|
+ conn.execute(
|
|
|
|
|
+ "UPDATE order_records SET status = ?, updated_at = ? WHERE bitstamp_order_id = ?",
|
|
|
|
|
+ (_normalize_status(status), _utc_now(), bitstamp_order_id),
|
|
|
|
|
+ )
|
|
|
|
|
+ conn.commit()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _validate_order_shape(market: str, side: str, order_type: str, amount, price) -> tuple[str, str | None, dict]:
|
|
def _validate_order_shape(market: str, side: str, order_type: str, amount, price) -> tuple[str, str | None, dict]:
|
|
|
meta = load_market_by_symbol(market)
|
|
meta = load_market_by_symbol(market)
|
|
|
if meta is None:
|
|
if meta is None:
|
|
@@ -61,8 +88,9 @@ def _validate_order_shape(market: str, side: str, order_type: str, amount, price
|
|
|
return amount_fmt, price_fmt, meta
|
|
return amount_fmt, price_fmt, meta
|
|
|
|
|
|
|
|
|
|
|
|
|
-def place_order(*, account_id: str, market: str, side: str, order_type: str, amount, price=None, expire_time: int | None = None, client_order_id: str | None = None) -> dict:
|
|
|
|
|
|
|
+def place_order(*, account_id: str, market: str, side: str, order_type: str, amount, price=None, expire_time: int | None = None, client_id: str | None = None, client_order_id: str | None = None) -> dict:
|
|
|
client = _get_client(account_id)
|
|
client = _get_client(account_id)
|
|
|
|
|
+ client_id = _normalize_client_id(client_id)
|
|
|
side = side.lower()
|
|
side = side.lower()
|
|
|
order_type = order_type.lower()
|
|
order_type = order_type.lower()
|
|
|
market = market.lower()
|
|
market = market.lower()
|
|
@@ -81,9 +109,15 @@ def place_order(*, account_id: str, market: str, side: str, order_type: str, amo
|
|
|
if order_type not in {"market", "limit"}:
|
|
if order_type not in {"market", "limit"}:
|
|
|
raise HTTPException(status_code=400, detail="invalid order_type")
|
|
raise HTTPException(status_code=400, detail="invalid order_type")
|
|
|
if side == "buy":
|
|
if side == "buy":
|
|
|
- result = client.trading.buy_gtd_order(amount=amount, price=price or "0", base=base, quote=quote, expire_time=expire_timestamp)
|
|
|
|
|
|
|
+ if expire_timestamp is None:
|
|
|
|
|
+ result = client.trading.buy_order(amount=amount, price=price or "0", base=base, quote=quote)
|
|
|
|
|
+ else:
|
|
|
|
|
+ result = client.trading.buy_gtd_order(amount=amount, price=price or "0", base=base, quote=quote, expire_time=expire_timestamp)
|
|
|
elif side == "sell":
|
|
elif side == "sell":
|
|
|
- result = client.trading.sell_gtd_order(amount=amount, price=price or "0", base=base, quote=quote, expire_time=expire_timestamp)
|
|
|
|
|
|
|
+ if expire_timestamp is None:
|
|
|
|
|
+ result = client.trading.sell_order(amount=amount, price=price or "0", base=base, quote=quote)
|
|
|
|
|
+ else:
|
|
|
|
|
+ result = client.trading.sell_gtd_order(amount=amount, price=price or "0", base=base, quote=quote, expire_time=expire_timestamp)
|
|
|
else:
|
|
else:
|
|
|
raise HTTPException(status_code=400, detail="invalid side")
|
|
raise HTTPException(status_code=400, detail="invalid side")
|
|
|
except BitstampError as exc:
|
|
except BitstampError as exc:
|
|
@@ -99,16 +133,63 @@ def place_order(*, account_id: str, market: str, side: str, order_type: str, amo
|
|
|
conn.execute(
|
|
conn.execute(
|
|
|
"""
|
|
"""
|
|
|
INSERT INTO order_records
|
|
INSERT INTO order_records
|
|
|
- (id, account_id, market, side, order_type, amount, price, expire_time, status, bitstamp_order_id, client_order_id, raw_json, created_at, updated_at)
|
|
|
|
|
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
|
+ (id, account_id, market, side, order_type, amount, price, expire_time, status, bitstamp_order_id, client_id, client_order_id, raw_json, created_at, updated_at)
|
|
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
""",
|
|
""",
|
|
|
- (record_id, account_id, market, side, order_type, amount, price, expire_time, str(result.get("status", "open")), bitstamp_order_id, client_order_id or result.get("client_order_id"), json.dumps(result), now, now),
|
|
|
|
|
|
|
+ (record_id, account_id, market, side, order_type, amount, price, expire_time, _normalize_status(result.get("status", "open")), bitstamp_order_id, client_id, client_order_id or result.get("client_order_id"), json.dumps(result), now, now),
|
|
|
)
|
|
)
|
|
|
conn.commit()
|
|
conn.commit()
|
|
|
|
|
|
|
|
return {"ok": True, "bitstamp_order_id": bitstamp_order_id, "record_id": record_id, "status": str(result.get("status", "open")), "raw": result}
|
|
return {"ok": True, "bitstamp_order_id": bitstamp_order_id, "record_id": record_id, "status": str(result.get("status", "open")), "raw": result}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def get_open_orders(*, account_id: str, client_id: str | None = None) -> dict:
|
|
|
|
|
+ client_id = _normalize_client_id(client_id)
|
|
|
|
|
+ with get_connection() as conn:
|
|
|
|
|
+ if client_id is None:
|
|
|
|
|
+ rows = conn.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT id, account_id, market, side, order_type, amount, price, expire_time, status, bitstamp_order_id, client_id, client_order_id, raw_json, created_at, updated_at
|
|
|
|
|
+ FROM order_records
|
|
|
|
|
+ WHERE account_id = ? AND lower(status) IN ('open', 'new', 'partially_filled')
|
|
|
|
|
+ ORDER BY created_at DESC
|
|
|
|
|
+ """,
|
|
|
|
|
+ (account_id,),
|
|
|
|
|
+ ).fetchall()
|
|
|
|
|
+ else:
|
|
|
|
|
+ rows = conn.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT id, account_id, market, side, order_type, amount, price, expire_time, status, bitstamp_order_id, client_id, client_order_id, raw_json, created_at, updated_at
|
|
|
|
|
+ FROM order_records
|
|
|
|
|
+ WHERE account_id = ? AND client_id = ? AND lower(status) IN ('open', 'new', 'partially_filled')
|
|
|
|
|
+ ORDER BY created_at DESC
|
|
|
|
|
+ """,
|
|
|
|
|
+ (account_id, client_id),
|
|
|
|
|
+ ).fetchall()
|
|
|
|
|
+ return {"ok": True, "client_id": client_id, "orders": [dict(row) for row in rows]}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def cancel_all_orders(*, account_id: str, client_id: str | None = None) -> dict:
|
|
|
|
|
+ client_id = _normalize_client_id(client_id)
|
|
|
|
|
+ orders = get_open_orders(account_id=account_id, client_id=client_id)["orders"]
|
|
|
|
|
+ results = []
|
|
|
|
|
+ for order in orders:
|
|
|
|
|
+ bitstamp_order_id = order.get("bitstamp_order_id")
|
|
|
|
|
+ if not bitstamp_order_id:
|
|
|
|
|
+ results.append({"order_id": None, "ok": False, "error": "missing bitstamp_order_id"})
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ results.append(cancel_order(account_id=account_id, order_id=bitstamp_order_id))
|
|
|
|
|
+ except HTTPException as exc:
|
|
|
|
|
+ detail = str(exc.detail)
|
|
|
|
|
+ if "not found" in detail.lower():
|
|
|
|
|
+ _set_local_order_status(bitstamp_order_id=bitstamp_order_id, status="missing")
|
|
|
|
|
+ results.append({"ok": False, "order_id": bitstamp_order_id, "error": detail, "status": "missing"})
|
|
|
|
|
+ continue
|
|
|
|
|
+ raise
|
|
|
|
|
+ return {"ok": True, "client_id": client_id, "cancelled": results, "count": len(results)}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def query_order(*, account_id: str, order_id, client_order_id: str | None = None, omit_transactions: bool | None = None) -> dict:
|
|
def query_order(*, account_id: str, order_id, client_order_id: str | None = None, omit_transactions: bool | None = None) -> dict:
|
|
|
order_id = str(order_id)
|
|
order_id = str(order_id)
|
|
|
client = _get_client(account_id)
|
|
client = _get_client(account_id)
|
|
@@ -120,7 +201,7 @@ def query_order(*, account_id: str, order_id, client_order_id: str | None = None
|
|
|
with get_connection() as conn:
|
|
with get_connection() as conn:
|
|
|
conn.execute(
|
|
conn.execute(
|
|
|
"UPDATE order_records SET status = ?, raw_json = ?, updated_at = ? WHERE bitstamp_order_id = ?",
|
|
"UPDATE order_records SET status = ?, raw_json = ?, updated_at = ? WHERE bitstamp_order_id = ?",
|
|
|
- (str(result.get("status", "unknown")), json.dumps(result), _utc_now(), order_id),
|
|
|
|
|
|
|
+ (_normalize_status(result.get("status", "unknown")), json.dumps(result), _utc_now(), order_id),
|
|
|
)
|
|
)
|
|
|
conn.commit()
|
|
conn.commit()
|
|
|
|
|
|