Stockbit Research research Playground @hansputera
carina.py — 9.6 KB Download
""" Carina API — Stockbit Trading API client. Handles orders, portfolio, balance, and trading operations. """ from typing import Optional, List import requests from .auth import AuthManager from .utils import RateLimiter, retry, generate_ui_ref from .models import CashBalance, PortfolioItem, Order class CarinaAPI: BASE = "https://carina.stockbit.com" def __init__(self, auth: AuthManager, session: requests.Session, rate_limiter: RateLimiter): self._auth = auth self._session = session self._rl = rate_limiter def login(self, pin: Optional[str] = None) -> dict: sekuritas_token = self._auth.get_sekuritas_token() return self._auth.login_trading(sekuritas_token, pin) @property def _headers(self): token = self._auth.ensure_trading_token() return {"Authorization": f"Bearer {token}"} # ── Balance ─────────────────────────────────────────── @retry() def get_balance(self) -> CashBalance: data = self._get("/balance/cash") d = data.get("data", {}) ach = d.get("available_cash_on_hand", d.get("available_cash", 0)) return CashBalance( cash_balance=ach, available_cash=ach, ) @retry() def get_balance_info(self) -> dict: return self._get("/balance/cash/info") @retry() def get_withdrawable(self) -> dict: return self._get("/balance/withdrawable") # ── Portfolio ───────────────────────────────────────── @retry() def get_portfolio_list(self) -> List[PortfolioItem]: body = self._get("/portfolio/v2/list") data = body.get("data", {}) results = data.get("results", data.get("data", [])) items = [] for item in results: if not isinstance(item, dict): continue qty = item.get("qty", {}) price = item.get("price", {}) asset = item.get("asset", {}).get("unrealised", {}) items.append(PortfolioItem( symbol=item.get("symbol", ""), total_shares=qty.get("stock_on_hand", 0), average_buy=price.get("average", {}).get("price", 0), last_price=price.get("latest", 0), total_value=asset.get("market_value", 0), amount_invested=item.get("asset", {}).get("amount_invested", 0), profit_loss=asset.get("profit_loss", 0), gain_pct=asset.get("gain", 0), )) return items @retry() def get_portfolio_summary(self) -> dict: return self._get("/portfolio/v2/summary") @retry() def get_portfolio_detail(self, stock_code: str) -> dict: return self._get("/portfolio/v2/detail", params={"stock_code": stock_code}) # ── Orders ──────────────────────────────────────────── @retry() def buy(self, symbol: str, price: int, shares: int, board_type: str = "RG") -> dict: return self._post("/order/v2/buy", json={ "symbol": symbol, "price": price, "shares": shares, "board_type": board_type, }) @retry() def buy_with_optional(self, symbol: str, price: int, shares: int, board_type: str = "RG", is_gtc: bool = False, time_in_force: str = "0", split_order: bool = False, platform_order_type: str = "PLATFORM_ORDER_TYPE_LIMIT_DAY") -> dict: return self._post("/order/v2/buy", json={ "symbol": symbol, "price": price, "shares": shares, "board_type": board_type, "is_gtc": is_gtc, "time_in_force": time_in_force, "split_order": split_order, "platform_order_type": platform_order_type, }) @retry() def sell(self, symbol: str, price: int, shares: int, board_type: str = "RG") -> dict: return self._post("/order/v2/sell", json={ "symbol": symbol, "price": price, "shares": shares, "board_type": board_type, }) @retry() def cancel(self, order_id: str) -> dict: return self._post("/order/v2/cancel", json={ "order_id": order_id, }) @retry() def amend(self, order_id: str, symbol: str, price: int, shares: int, board_type: str = "RG") -> dict: return self._post("/order/v2/amend", json={ "order_id": order_id, "symbol": symbol, "price": price, "shares": shares, "board_type": board_type, }) @retry() def bulk_cancel(self, order_ids: List[str]) -> dict: return self._post("/order/v2/bulk-cancel", json={"order_ids": order_ids}) @retry() def get_order_detail(self, order_id: str) -> dict: return self._get("/order/v2/detail", params={"order_id": order_id}) @retry() def get_order_list(self, stock_code: Optional[str] = None) -> dict: params = {} if stock_code: params["filter_criteria.stock_code"] = stock_code return self._get("/order/v2/list", params=params) @retry() def get_order_history_v4(self) -> dict: return self._get("/order/history/v4") # ── Account ─────────────────────────────────────────── @retry() def get_trading_info(self) -> dict: return self._get("/trading/info") @retry() def get_account_bank(self) -> dict: return self._get("/account/bank") @retry() def get_account_personal(self) -> dict: return self._get("/account/personal") # ── History ─────────────────────────────────────────── @retry() def get_history_realized(self) -> dict: return self._get("/history/realized") @retry() def get_history_realized_detail(self) -> dict: return self._get("/history/realized/detail") @retry() def get_cash_deposit_history(self) -> dict: return self._get("/cash/deposit/history") @retry() def get_cash_withdraw_history(self) -> dict: return self._get("/cash/withdraw/history/v2") # ── Day Trade ───────────────────────────────────────── @retry() def day_trade_buy(self, symbol: str, price: int, shares: int) -> dict: return self._post("/order/day-trade/v1/buy", json={ "ui_ref": generate_ui_ref(), "symbol": symbol, "price": price, "shares": shares, }) @retry() def day_trade_sell(self, symbol: str, price: int, shares: int) -> dict: return self._post("/order/day-trade/v1/sell", json={ "ui_ref": generate_ui_ref(), "symbol": symbol, "price": price, "shares": shares, }) # ── Smart Orders ────────────────────────────────────── @retry() def bracket_order(self, symbol: str, entry_price: int, shares: int, take_profit: int, stop_loss: int) -> dict: return self._post("/smart-order/bracket-order/v1/order/", json={ "symbol": symbol, "entry_price": entry_price, "shares": shares, "take_profit_price": take_profit, "stop_loss_price": stop_loss, }) @retry() def stop_order(self, symbol: str, trigger_price: int, shares: int, order_type: str = "SELL") -> dict: return self._post("/smart-order/stop-order/v1/order/", json={ "symbol": symbol, "trigger_price": trigger_price, "shares": shares, "order_type": order_type, }) # ── Stock Tradability ───────────────────────────────── @retry() def check_tradable(self, stock_codes: str) -> dict: return self._get("/stock/tradable", params={"stock_codes": stock_codes}) # ── Internals ──────────────────────────────────────── def _get(self, path: str, **kwargs) -> dict: wait = self._rl.acquire() if wait > 0: import time time.sleep(wait) headers = kwargs.pop("headers", {}) headers.update(self._headers) resp = self._session.get(f"{self.BASE}{path}", headers=headers, **kwargs) resp.raise_for_status() return resp.json() def _post(self, path: str, **kwargs) -> dict: wait = self._rl.acquire() if wait > 0: import time time.sleep(wait) headers = kwargs.pop("headers", {}) headers.update(self._headers) headers["Content-Type"] = "application/json" body = kwargs.pop("json", {}) if "ui_ref" not in body: body["ui_ref"] = generate_ui_ref() resp = self._session.post(f"{self.BASE}{path}", json=body, headers=headers, **kwargs) resp.raise_for_status() return resp.json()