Metadata-Version: 2.5
Name: nexusquant-sdk
Version: 0.2.1
Summary: NexusQuant strategy provider Python SDK
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: platformdirs>=4.2
Description-Content-Type: text/markdown

# nexusquant-sdk

NexusQuant strategy provider Python SDK.

## Installation

```bash
pip install nexusquant-sdk
```

Requires Python 3.10 or newer.

## Quick start

```python
import nexusquant_sdk as nq

# Authenticate once (opens browser)
nq.auth_login()

# List your strategies
strategies = nq.strategy_list()

# Send a signal
nq.strategy_send_signal_single(
    "my_alpha",
    ticker="AAPL",
    direction="buy",
    price=150.0,
    quantity=100,
    order_type="LIMIT",
)
```

## Authentication

Login uses the OAuth 2.0 PKCE flow against Cognito. Tokens are stored locally at
the path returned by `nq.auth_credentials_path()`, so you only log in once.

```python
nq.auth_login()           # opens Cognito Hosted UI, saves tokens
nq.auth_logged_in()       # True if a token is present
nq.auth_refresh()         # force-refresh the ID token via refresh token
nq.auth_logout()          # delete local tokens
nq.auth_credentials_path() # path to the credentials file
```

Override defaults with environment variables:

| Variable | Default |
|---|---|
| `NEXUSQUANT_API_ENDPOINT` | `https://api.nexusquant.co` |
| `NEXUSQUANT_COGNITO_DOMAIN` | `auth.lookatwallstreet.com` |
| `NEXUSQUANT_COGNITO_CLIENT_ID` | *(built-in)* |
| `NEXUSQUANT_REDIRECT_URI` | `http://127.0.0.1:8251/callback` |

## Strategy API

```python
# Register / update a strategy
nq.strategy_register("my_alpha", "My Alpha Strategy", output_unit="SHARE_COUNT")

# List strategies
nq.strategy_list()

# Signal history
nq.strategy_signal_history("my_alpha", limit=20)

# Send a single signal
nq.strategy_send_signal_single(
    "my_alpha",
    ticker="AAPL",
    direction="buy",   # "buy" or "sell"
    price=150.0,
    quantity=100,      # share count
    order_type="LIMIT" # "MARKET" or "LIMIT"
)

# Send a multi-route signals map
nq.strategy_send_signals("my_alpha", {"default": {...}})

# ...or the same map from a JSON file
nq.strategy_send_signals_from_path("my_alpha", "signals.json")

# Subscriber configuration snapshot
nq.strategy_subscriber_config("my_alpha")
```

## SizingPolicy

A SizingPolicy replaces the fixed `quantity` on a signal with a **formula** that
decides the order quantity — and optionally a limit price — at order time. You
publish the formula once; each signal then carries only the parameter values it
needs. Anything that depends on the individual subscriber's account is never sent
by you: it is bound inside that user's own container the moment the order is placed.

### What a policy is made of

Two things, and nothing else:

- **`expr`** — the formulas, keyed by output slot.
- **`params`** — the names you promise to supply with every signal. Just the names.

```python
nq.policy_publish(
    "my_alpha",
    ticker="TQQQ",
    expr={
        "f_depth":       "exp(-k_depth * depth / 10)",
        "buy.quantity":  "target_shares * f_depth * clamp(1 - pos_ratio, 0, 1)",
        "buy.price":     "ref_price * (1 - slip)",
        "sell.quantity": "held_shares * exit_frac",
    },
    params=["target_shares", "k_depth", "depth", "ref_price", "slip", "exit_frac"],
)
```

Intermediate names (`f_depth` above) are fine — only the slot names below are read
as output — but an `expr` must define **at least one output slot**, and an
intermediate name may not contain a dot. `ticker` is passed separately because it
is not part of the artifact: the same formula pointed at another symbol is the same
formula.

### Output slots

`nq.OUTPUT_SLOTS` is the full set:

| Slot | What the formula yields |
|---|---|
| `buy.quantity` / `sell.quantity` | **Share count** (floored) |
| `buy.price` / `sell.price` | **Limit price** — supplied ⇒ limit order, omitted ⇒ market order |

The container picks the slot matching the signal's direction. A formula with only
buy slots does not block a sell signal — it simply has no opinion on that
direction, and the `quantity` you sent with the signal is used instead.

### Account state is not yours to send

`nq.ACCOUNT_FEATURES` holds the per-user names — `pos_ratio`, `held_shares`,
`account_value`, `cash_available`, `buying_power`, `pos_pct_of_account`,
`position_value`, `allocation_usd`, `pending_sell_shares`. Your formula may
**reference** them, but you must never supply a value for one: those values do not
leave the execution plane. Sending one is rejected, whether or not your formula
uses it.

`nq.policy_features()` is the authority on which names exist at all, and on which
account-state names the container can currently supply (`suppliable`) — a name
that exists but cannot be supplied will pass your editor and fail at publish. Call
it before writing a formula:

```python
nq.policy_features()   # {features, shared, account, functions, output_slots, guidance}
```

The SDK carries fallback copies of the account-name set and the whitelisted DSL
functions for when the registry cannot be reached, but those go stale as the server
registry grows — when the fallback is used, local success does **not** imply the
server will accept.

### Publishing, listing, stopping

```python
# Publishing IS the go-live action. There is no shadow or canary step, and the
# previous version on the same slot is deactivated.
nq.policy_publish("my_alpha", ticker="TQQQ", expr={...}, params=[...], profile="normal")

# Everything published for one strategy: full artifact, slots, params, account names used
nq.policy_list("my_alpha", ticker="TQQQ", mode="active")

# Emergency stop for a live version — not a promotion step, since publish already went live
nq.policy_set_mode("TQQQ/normal/…/abc123", "off")   # "active" to restore
```

`policy_publish(..., check_names=False)` skips fetching the registry before
validating locally. Leave it on: forgetting to list a name in `params` is the most
common mistake, and catching it locally beats being rejected after the fact.

### Sending a signal against a policy

```python
nq.policy_send_signal(
    "my_alpha",
    ticker="TQQQ",
    price=51.2,
    direction="buy",
    params={
        "target_shares": 10, "k_depth": 0.5, "depth": 3,
        "ref_price": 51.2, "slip": 0.002, "exit_frac": 0.25,
    },
)
```

By default this fetches the active policy first and uses it to validate and coerce
your parameters. Three checks, each mirroring the server:

| Situation | Result | Why not something else |
|---|---|---|
| A declared parameter is missing | Error | No defaults — a default turns "unknown" into "known" |
| A name you did not declare | Error | The formula cannot reference it; extra names mean the two sides disagree |
| An account-state name | Error | Its value never leaves the execution plane |

`fetch_spec=False` skips the fetch, and then **only the account-state rule is
checked** — the rest is unknowable locally, so it is not pretended. Pass a
`PolicySpec` yourself via `spec=` to validate many signals without refetching:

```python
spec = nq.policy_spec_for("my_alpha", ticker="TQQQ")
for bar in bars:
    nq.policy_send_signal("my_alpha", ticker="TQQQ", price=bar.close,
                          direction="buy", params={...}, spec=spec)
```

`policy_spec_for` refuses to guess: if several profiles are active on the same
ticker it raises rather than pick one, since their declared parameters can differ.
Pass `policy_id=` to name the version you mean.

Validation failures raise `nq.SignalValidationError`.

### Two things to keep in mind

**The server is always the authority.** Passing local validation does not mean the
server will accept — policy state and review status live there, not here. This
layer exists only to move the obvious mistakes onto your machine, where you can see
which field is wrong instead of guessing from a rejection.

**There is no publish-time bound on order size.** Parameters are names only, with
no declared domain, so nothing proves at publish time that your formula stays under
a limit. How large an order it can place is clamped at order time by the
subscriber's own `max_order_cash_usd`. Keep your formulas in a sane range yourself.

## Requirements

- Python 3.10+
- `httpx >= 0.27`
- `platformdirs >= 4.2`
