Metadata-Version: 2.4
Name: paperbroker-client
Version: 0.2.7
Summary: Python client for PaperBroker
Author-email: LoiTran <loi.tran@algotrade.vn>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: quickfix>=1.15.1
Requires-Dist: requests>=2.32.0
Requires-Dist: redis>=4.5.0
Requires-Dist: kafka-python-ng>=2.2.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: cli
Requires-Dist: pyyaml>=6.0; extra == "cli"

# PaperBroker Client

Python client for algorithmic trading via FIX 4.4 + REST, with an
event-driven core and a multi-instrument alpha framework on top.

> Optimized for Linux. On Apple Silicon: use REST-only mode
> (`PaperBrokerClient(enable_fix=False)`), Docker, or Rosetta — QuickFIX
> binary isn't natively compatible.

## Install

```bash
pip install paperbroker-client                            # core
pip install 'redis[asyncio]>=5.0.0'                       # + Redis MD
pip install 'kafka-python-ng>=2.2.0'                      # + Kafka MD
pip install 'paperbroker-client[cli]'                     # + YAML CLI
```

## Quick start

```python
import os
from dotenv import load_dotenv
from paperbroker import PaperBrokerClient

load_dotenv()

client = PaperBrokerClient(
    default_sub_account=os.getenv("PAPER_ACCOUNT_ID_D1"),
    username=os.getenv("PAPER_USERNAME"),
    password=os.getenv("PAPER_PASSWORD"),
    rest_base_url=os.getenv("PAPER_REST_BASE_URL"),
    socket_connect_host=os.getenv("SOCKET_HOST"),
    socket_connect_port=int(os.getenv("SOCKET_PORT", "5001")),
    sender_comp_id=os.getenv("SENDER_COMP_ID"),
    target_comp_id=os.getenv("TARGET_COMP_ID"),
)

client.on("fix:order:filled", lambda cl_ord_id, last_px, last_qty, **kw:
          print(f"filled {last_qty} @ {last_px}"))

client.connect()
if not client.wait_until_logged_on(timeout=10):
    raise RuntimeError(client.last_logon_error())

# place_order defaults to tif="DAY" (Vietnam venues require this; v0.2.7+)
order_id = client.place_order(
    full_symbol=os.getenv("VN30F1M", "HNXDS:VN30F2606"),
    side="BUY", qty=1, price=1950.0,
)
```

End-to-end walkthrough: [examples/01_simple_login.py](examples/01_simple_login.py).

## Examples (15 total)

| # | File | Demonstrates |
|---|---|---|
| 01 | [01_simple_login.py](examples/01_simple_login.py) | FIX logon + cash query |
| 02 | [02_multi_order_cancel.py](examples/02_multi_order_cancel.py) | Place N, cancel actives, handle rejects + tuple `cancel_order` return |
| 03 | [03_cross_matching.py](examples/03_cross_matching.py) | D1↔D2 multi-account fills (partial + full) |
| 04 | [04_market_data_query.py](examples/04_market_data_query.py) | Redis one-shot quote query |
| 05 | [05_market_data_subscribe.py](examples/05_market_data_subscribe.py) | Redis pub/sub MERGED (flip `merge_updates=False` for RAW) |
| 06 | [06_account_state.py](examples/06_account_state.py) | Portfolio + orders + transactions |
| 07 | [07_max_placeable.py](examples/07_max_placeable.py) | Position sizing under margin + fees |
| 08 | [08_market_data_kafka.py](examples/08_market_data_kafka.py) | Kafka streaming (recommended for multi-instrument alphas) |
| **Alpha framework (v0.2.7+)** | | |
| 09 | [09_alpha_rsi_1m.py](examples/09_alpha_rsi_1m.py) + `.yaml` | Single-instrument template, RSI-50 in-signal exit |
| 10 | [10_alpha_bollinger_bands.py](examples/10_alpha_bollinger_bands.py) | BB + `StateStore` sticky-lock (re-entry guard across restart) |
| 11 | [11_alpha_exit_patterns.py](examples/11_alpha_exit_patterns.py) | TP / SL / drawdown / EOD all in `get_signals` |
| 12 | [12_alpha_pair_spread.py](examples/12_alpha_pair_spread.py) | Multi-instrument joint trigger; atomic 2-leg signals — Gate 1.5 pilot |
| 13 | [13_alpha_cross_asset.py](examples/13_alpha_cross_asset.py) | Signal on X, order on Y via `plan_orders` override |
| 14 | [14_alpha_iceberg.py](examples/14_alpha_iceberg.py) | One signal → N staggered legs via `plan_orders` |
| 15 | [15_alpha_mm_basic.py](examples/15_alpha_mm_basic.py) | Quote-driven market-making (2-sided quoting, cancel-then-place, `trigger_on_quote=True`) — v0.2.7 Milestone E |

## Building an alpha

```python
from paperbroker.alpha import AlphaContext, Signal, SignalDrivenAlpha

class RSI1MAlpha(SignalDrivenAlpha):
    declared_params = {"rsi_period", "oversold", "overbought"}

    def get_indicators(self, ctx: AlphaContext):
        sym = self.config.instruments[0]
        closes = [b.close for b in ctx.bars[sym]]
        return {"rsi": _rsi(closes, int(self.config.params["rsi_period"]))}

    def get_signals(self, indicators, ctx):
        sym, rsi = self.config.instruments[0], indicators["rsi"]
        if rsi is None: return []
        pos = ctx.positions.get(sym)
        if pos and abs(pos.quantity) > 1e-9:
            if pos.quantity > 0 and rsi > 50: return [Signal(sym, "SELL", tag="exit")]
            if pos.quantity < 0 and rsi < 50: return [Signal(sym, "BUY",  tag="exit")]
            return []
        if rsi < 30: return [Signal(sym, "BUY",  tag="entry")]
        if rsi > 70: return [Signal(sym, "SELL", tag="entry")]
        return []

    def get_entry_price(self, signal, ctx): return ctx.bars[signal.symbol][-1].close
    def get_quantity(self, signal, ctx):    return self.config.qty_for(signal.symbol)

alpha = RSI1MAlpha.from_paper(
    instruments=[os.getenv("VN30F1M", "HNXDS:VN30F2606")],
    sub_account="main",
    timeframe="1m",
    qty=1,
    params={"rsi_period": 14, "oversold": 30, "overbought": 70},
)
alpha.run()
```

Full guide: **[alpha-guide.md](./alpha-guide.md)** — `AlphaConfig`,
`AlphaContext`, hook chain, joint trigger across multiple instruments,
in-signal exits via `CloseSignal` / `TakeProfitSignal` / `StopLossSignal`,
state tracking (`OrderState` / `SignalState`), trigger pluggability
(`trigger_on_quote` for MM), `plan_orders` for iceberg & cross-asset,
YAML CLI, market-data backend choice.

**State the alpha sees via `ctx` (v0.2.7 Milestone E):**

| Field | Type | Notes |
|---|---|---|
| `ctx.open_orders[sym]` | `list[OrderState]` | Working orders per symbol (lifecycle state: status, cum_qty, avg_px, remaining_qty) |
| `ctx.signals[parent_signal_id]` | `SignalState` | Aggregated state across all legs from one Signal |
| `ctx.positions[sym]` | `Position` | Signed qty + VWAP from fill events |
| `ctx.account` | `AccountState` | cash / equity / margin_used (1s TTL cache) |
| `ctx.quotes[sym]` | `QuoteSnapshot` | Latest L1/L2 quote |
| `ctx.bars[sym]` | `Deque[Bar]` | Closed-bar history |
| `ctx.triggered_by` | `list[str]` | Which symbols' bar/quote fired this trigger |

## Environment variables

```bash
# FIX + REST
SOCKET_HOST=<host>
SOCKET_PORT=<port>
SENDER_COMP_ID=<id>
TARGET_COMP_ID=<id>
PAPER_USERNAME=<user>
PAPER_PASSWORD=<pass>
PAPER_REST_BASE_URL=<url>

# Trading account
PAPER_ACCOUNT_ID_D1=<primary-sub-account>
# PAPER_ACCOUNT_ID_D2=<secondary>            # example 03 only

# Market data
MARKET_REDIS_HOST=<host>
MARKET_REDIS_PORT=<port>
MARKET_REDIS_PASSWORD=<password>
PAPERBROKER_KAFKA_BOOTSTRAP_SERVERS=<host>:<port>
PAPERBROKER_KAFKA_USERNAME=<sasl-user>
PAPERBROKER_KAFKA_PASSWORD=<sasl-pass>
PAPERBROKER_ENV_ID=real

# Instruments (update monthly — see .env.example)
VN30F1M=HNXDS:VN30F2606
VN30F2M=HNXDS:VN30F2609
EQUITY=HSX:MWG
```

Never commit `.env`. Use distinct `order_store_path` per running
instance.

## API surface

| Capability | Methods |
|---|---|
| Lifecycle | `connect()` · `disconnect()` · `wait_until_logged_on(timeout)` · `is_logged_on()` · `last_logon_error()` |
| Orders (FIX) | `place_order(full_symbol, side, qty, price, ord_type, tif="DAY")` · `cancel_order(cl_ord_id, timeout) -> (is_terminal, status)` · `recover_pending_orders()` (v0.2.4+) · `is_order_done(cl_ord_id)` · `wait_for(cl_ord_id, statuses, timeout)` |
| Account (REST) | `get_cash_balance()` · `get_account_balance()` · `get_portfolio_by_sub(sub_id)` · `get_orders(start, end)` · `get_transactions_by_date(start, end)` · `get_max_placeable(symbol, price, side)` |
| Sub-account | `use_sub_account(sub_id)` (context manager) · `set_default_sub_account(sub_id)` · `current_sub_account()` |
| Events | `on(event, handler)` · `off(event, handler)` |

TIF default `"DAY"` (v0.2.7+) — Vietnam venues reject GTC. See
[config-guide.md](./config-guide.md#time-in-force-v027) for the table.

## Next steps

- **[alpha-guide.md](./alpha-guide.md)** — `SignalDrivenAlpha` framework
- **[config-guide.md](./config-guide.md)** — connection params + env
- **[event-guide.md](./event-guide.md)** — FIX event reference
- **[market-data-guide.md](./market-data-guide.md)** — Redis & Kafka clients

Requirements: Python 3.8+ · quickfix ≥ 1.15.1 · requests ≥ 2.32 ·
python-dotenv ≥ 1.0 · redis ≥ 4.5 (optional) · kafka-python-ng ≥ 2.2
(optional).

## License

See LICENSE.
