README.md

<p align="center">
  <img src="assets/alpa_ex.png" alt="alpa_ex" width="300">
</p>

# alpa_ex

[![Hex.pm](https://img.shields.io/hexpm/v/alpa_ex.svg)](https://hex.pm/packages/alpa_ex)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/alpa_ex)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Elixir client library for the [Alpaca Trading API](https://alpaca.markets/).

Commission-free stock, options, and crypto trading with real-time market data.

## Features

- **Trading API** - Account management, orders, positions, watchlists
- **Market Data API** - Historical and real-time bars, quotes, trades, snapshots
- **WebSocket Streaming** - Real-time trade updates and market data
- **Options Trading** - Option contract search and trading
- **Crypto Trading** - 24/7 cryptocurrency trading
- **TypedStruct Models** - Fully typed responses with Decimal precision
- **Crypto Market Data** - Bars, quotes, trades, snapshots, and order books
- **Corporate Actions** - Announcements and corporate action tracking
- **Pagination Helpers** - Eager `all/2` and lazy `stream/2` for paginated endpoints
- **Telemetry** - Built-in `:telemetry` events for observability
- **Modern Stack** - Elixir 1.14+, Req HTTP client, WebSockex

## Installation

Add `alpa_ex` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:alpa_ex, "~> 1.0"}
  ]
end
```

## Configuration

Set your API credentials as environment variables:

```bash
export APCA_API_KEY_ID="your-api-key"
export APCA_API_SECRET_KEY="your-api-secret"
export APCA_USE_PAPER="true"  # Use paper trading (default: true)
```

Or configure in your application:

```elixir
# config/runtime.exs
config :alpa_ex,
  api_key: System.get_env("APCA_API_KEY_ID"),
  api_secret: System.get_env("APCA_API_SECRET_KEY"),
  use_paper: true
```

## Quick Start

```elixir
# Get account info
{:ok, account} = Alpa.account()
IO.puts("Buying power: $#{account.buying_power}")

# Check if market is open
{:ok, open?} = Alpa.market_open?()

# Place a market order
{:ok, order} = Alpa.buy("AAPL", 10)

# Get positions
{:ok, positions} = Alpa.positions()

# Get market data
{:ok, bars} = Alpa.bars("AAPL", timeframe: "1Day", limit: 30)

# Get a market snapshot
{:ok, snapshot} = Alpa.snapshot("AAPL")
IO.puts("AAPL last trade: $#{snapshot.latest_trade.price}")
```

## Trading

### Orders

```elixir
# Market orders
{:ok, order} = Alpa.buy("AAPL", 10)
{:ok, order} = Alpa.sell("AAPL", 10)

# Limit order
{:ok, order} = Alpa.place_order(
  symbol: "AAPL",
  qty: 10,
  side: "buy",
  type: "limit",
  limit_price: "150.00",
  time_in_force: "gtc"
)

# Bracket order (entry with take-profit and stop-loss)
{:ok, order} = Alpa.place_order(
  symbol: "AAPL",
  qty: 10,
  side: "buy",
  type: "market",
  time_in_force: "day",
  order_class: "bracket",
  take_profit: %{limit_price: "160.00"},
  stop_loss: %{stop_price: "140.00", limit_price: "139.00"}
)

# List orders
{:ok, orders} = Alpa.orders(status: "open")

# Cancel orders
{:ok, _} = Alpa.cancel_order(order_id)
{:ok, _} = Alpa.cancel_all_orders()
```

### Positions

```elixir
# List all positions
{:ok, positions} = Alpa.positions()

# Get specific position
{:ok, position} = Alpa.position("AAPL")

# Close positions
{:ok, order} = Alpa.close_position("AAPL")
{:ok, order} = Alpa.close_position("AAPL", percentage: 50)  # Close 50%
{:ok, _} = Alpa.close_all_positions()
```

### Account

```elixir
# Account info
{:ok, account} = Alpa.account()

# Portfolio history
{:ok, history} = Alpa.history(period: "1M", timeframe: "1D")

# Account configuration
{:ok, config} = Alpa.account_config()
```

## Market Data

### Historical Data

```elixir
# Bars (OHLCV)
{:ok, bars} = Alpa.bars("AAPL",
  timeframe: "1Day",
  start: ~U[2024-01-01 00:00:00Z],
  limit: 100
)

# Quotes
{:ok, quotes} = Alpa.quotes("AAPL",
  start: ~U[2024-01-15 14:30:00Z],
  limit: 100
)

# Trades
{:ok, trades} = Alpa.trades("AAPL",
  start: ~U[2024-01-15 14:30:00Z],
  limit: 100
)

# Multi-symbol
{:ok, bars} = Alpa.MarketData.Bars.get_multi(
  ["AAPL", "MSFT", "GOOGL"],
  timeframe: "1Day"
)
```

### Latest Data

```elixir
# Latest bar, quote, trade
{:ok, bar} = Alpa.latest_bar("AAPL")
{:ok, quote} = Alpa.latest_quote("AAPL")
{:ok, trade} = Alpa.latest_trade("AAPL")

# Snapshot (all latest data combined)
{:ok, snapshot} = Alpa.snapshot("AAPL")
# => %Alpa.Models.Snapshot{
#      latest_trade: %Alpa.Models.Trade{...},
#      latest_quote: %Alpa.Models.Quote{...},
#      minute_bar: %Alpa.Models.Bar{...},
#      daily_bar: %Alpa.Models.Bar{...},
#      prev_daily_bar: %Alpa.Models.Bar{...}
#    }

# Multiple snapshots
{:ok, snapshots} = Alpa.snapshots(["AAPL", "MSFT", "GOOGL"])
```

## Real-Time Streaming

### Trade Updates (Order Status)

```elixir
# Stream order fills, cancellations, etc.
{:ok, pid} = Alpa.Stream.TradeUpdates.start_link(
  callback: fn event ->
    IO.puts("#{event.event}: #{event.order.symbol} @ #{event.price}")
  end
)

# Events: new, fill, partial_fill, canceled, expired, replaced, rejected
```

### Market Data Streaming

```elixir
# Start the stream
{:ok, pid} = Alpa.Stream.MarketData.start_link(
  callback: fn event ->
    case event.type do
      :trade -> IO.puts("Trade: #{event.data.symbol} @ #{event.data.price}")
      :quote -> IO.puts("Quote: #{event.data.symbol} bid=#{event.data.bid_price}")
      :bar -> IO.puts("Bar: #{event.data.symbol} close=#{event.data.close}")
    end
  end,
  feed: "iex"  # or "sip" for all exchanges
)

# Subscribe to symbols
Alpa.Stream.MarketData.subscribe(pid,
  trades: ["AAPL", "MSFT"],
  quotes: ["AAPL"],
  bars: ["SPY"]
)

# Unsubscribe
Alpa.Stream.MarketData.unsubscribe(pid, trades: ["MSFT"])

# Stop
Alpa.Stream.MarketData.stop(pid)
```

## Options Trading

```elixir
# Search for option contracts
{:ok, result} = Alpa.Options.Contracts.search("AAPL",
  type: :call,
  expiration_date_gte: ~D[2024-03-01],
  strike_price_gte: "150",
  strike_price_lte: "200"
)

# Get specific contract
{:ok, contract} = Alpa.Options.Contracts.get("AAPL240315C00175000")

# Trade options using regular order functions
{:ok, order} = Alpa.place_order(
  symbol: "AAPL240315C00175000",
  qty: 1,
  side: "buy",
  type: "limit",
  limit_price: "5.00",
  time_in_force: "day"
)
```

## Crypto Trading

```elixir
# List crypto assets
{:ok, assets} = Alpa.Crypto.Trading.assets()

# Buy/sell crypto
{:ok, order} = Alpa.Crypto.Trading.buy("BTC/USD", "0.001")
{:ok, order} = Alpa.Crypto.Trading.sell("ETH/USD", "0.1")

# Buy by dollar amount
{:ok, order} = Alpa.Crypto.Trading.buy_notional("BTC/USD", "100")  # $100 of BTC

# Crypto positions
{:ok, positions} = Alpa.Crypto.Trading.positions()
```

## Modules

| Module | Description |
|--------|-------------|
| `Alpa` | Main facade with delegated functions |
| `Alpa.Trading.Account` | Account info, config, activities, portfolio history |
| `Alpa.Trading.Orders` | Order placement and management |
| `Alpa.Trading.Positions` | Position management |
| `Alpa.Trading.Assets` | Asset information |
| `Alpa.Trading.Watchlists` | Watchlist CRUD |
| `Alpa.Trading.Market` | Market clock and calendar |
| `Alpa.Trading.CorporateActions` | Corporate action announcements |
| `Alpa.MarketData.Bars` | Historical bar data |
| `Alpa.MarketData.Quotes` | Historical quote data |
| `Alpa.MarketData.Trades` | Historical trade data |
| `Alpa.MarketData.Snapshots` | Market snapshots |
| `Alpa.Stream.TradeUpdates` | Real-time trade updates |
| `Alpa.Stream.MarketData` | Real-time market data |
| `Alpa.Options.Contracts` | Options contract search |
| `Alpa.Crypto.Trading` | Crypto trading operations |
| `Alpa.Crypto.MarketData` | Crypto bars, quotes, trades, snapshots, order books |
| `Alpa.Crypto.Funding` | Crypto transfers and wallets |
| `Alpa.Helpers` | Shared parse helpers (decimal, datetime, date) |
| `Alpa.Resource` | Macro for generating typed CRUD modules |

## Error Handling

All API functions return `{:ok, result}` or `{:error, %Alpa.Error{}}`:

```elixir
case Alpa.account() do
  {:ok, account} ->
    IO.puts("Balance: $#{account.cash}")

  {:error, %Alpa.Error{type: :unauthorized}} ->
    IO.puts("Check your API credentials")

  {:error, %Alpa.Error{type: :rate_limited}} ->
    IO.puts("Rate limited, try again later")

  {:error, error} ->
    IO.puts("Error: #{error}")
end
```

Error types: `:unauthorized`, `:forbidden`, `:not_found`, `:unprocessable_entity`, `:rate_limited`, `:server_error`, `:network_error`, `:timeout`

## Configuration Options

| Option | Environment Variable | Fallback | Default |
|--------|---------------------|----------|---------|
| `api_key` | `APCA_API_KEY_ID` | `ALPACA_API_KEY` | - |
| `api_secret` | `APCA_API_SECRET_KEY` | `ALPACA_API_SECRET` | - |
| `use_paper` | `APCA_USE_PAPER` | - | `true` |
| `timeout` | - | - | `30_000` |
| `receive_timeout` | - | - | `30_000` |

Options can be passed to any function to override config:

```elixir
Alpa.account(api_key: "other-key", api_secret: "other-secret")
```

## Development

```bash
# Run tests
mix test

# Run tests with coverage
mix coveralls

# Generate docs
mix docs

# Run static analysis
mix credo
mix dialyzer
```

## License

MIT License - see [LICENSE](https://github.com/phiat/alpa_ex/blob/main/LICENSE) for details.

## Links

- [Hex Docs](https://hexdocs.pm/alpa_ex)
- [Alpaca API Documentation](https://docs.alpaca.markets/)
- [Previous version (alpa)](https://github.com/phiat/alpa)