# PolymarketNotify

Notification **controls + channels** for Elixir trading bots. A small,
dependency-light layer that decides *what* to notify about, *for which
strategy*, and *where* to send it — then fans a plain-text message out
to pluggable delivery channels (Telegram, Discord, generic webhook, or
the Logger).

It was extracted from a Polymarket prediction-market trading bot and
generalized: nothing here is Polymarket-specific. Strategies are
identified by an arbitrary string **tag**, so a user-written strategy
gets its own notification settings with no code change.

This package is part of the Polymarket Elixir ecosystem (alongside
[`paper_ex`](https://hex.pm/packages/paper_ex) and friends), but it has
no dependency on any of the others and works with any bot.

## Two layers

- **Controls** (`PolymarketNotify.Controls`) — the core: a pure data
  model of per-**category** defaults (`:fills`, `:resolutions`,
  `:errors`, `:daily_summary`, `:consensus`) with per-**strategy**
  overrides. Route `fills` for the AI strategy to Telegram while muting
  `fills` for the surge bots, apply a `min_value` threshold, etc. Pure
  data + pure resolution — no I/O, trivially testable, and a UI/editor
  can read and write it later.
- **Channels** (`PolymarketNotify.Channel`) — pluggable delivery. A
  channel is any module implementing the `deliver/2` behaviour that
  ships text somewhere. Built-ins: `Telegram`, `Discord`, `Webhook`,
  `Log`, and a `Multi` fan-out. Bring your own by implementing the
  behaviour.

## Design notes

- **Best-effort.** `PolymarketNotify.notify/3` consults the controls
  and, if the `(category, strategy, value)` passes, fans the message
  out. Nothing configured / not allowed ⇒ a silent no-op. It **never
  raises into the caller** and always returns `:ok`; a channel that
  errors or raises is isolated.
- **No process, no state.** The controls are plain maps you own and
  persist however you like. The library adds no supervision tree.
- **Injectable HTTP.** Every network channel accepts an `:http`
  function `(url, json -> {:ok, status} | {:error, term})`, so tests
  need no real network and you can swap the client.
- **Secret-aware config.** Any option value may be given as
  `{:system, "VAR"}` to read from the environment at delivery time
  instead of baking a token into config.

## Installation

Add `polymarket_notify` to your deps in `mix.exs`:

```elixir
def deps do
  [
    {:polymarket_notify, "~> 0.1"}
  ]
end
```

## Usage

Configure channels and (optionally) a default control set:

```elixir
config :polymarket_notify,
  channels: %{
    telegram: {PolymarketNotify.Telegram,
               [bot_token: {:system, "TELEGRAM_BOT_TOKEN"},
                chat_id: {:system, "TELEGRAM_CHAT_ID"}]},
    log: {PolymarketNotify.Log, []}
  },
  controls: PolymarketNotify.Controls.default()
```

Then notify from anywhere in your bot:

```elixir
# Simple: consults app config for controls + channels.
PolymarketNotify.notify(:fills, "Filled 100 @ 0.62 (BTC-up)")

# With a strategy tag (for per-strategy overrides) and a magnitude
# (for the category's :min_value threshold):
PolymarketNotify.notify(:fills, "Big fill!", strategy: "ai", value: 250.0)

# Inject controls/channels directly (handy in tests):
PolymarketNotify.notify(:errors, "boom",
  controls: my_controls,
  channels: %{log: {PolymarketNotify.Log, [level: :error]}})
```

### The control model

```elixir
%{
  categories: %{
    fills:         %{enabled: true, min_value: 25.0, channels: [:telegram]},
    resolutions:   %{enabled: true, channels: [:telegram]},
    errors:        %{enabled: true, channels: [:telegram, :log]},
    daily_summary: %{enabled: true, channels: [:telegram]}
  },
  # per-strategy overrides: tag => %{category => partial pref}
  strategies: %{
    "surge" => %{fills: %{enabled: false}},
    "ai"    => %{fills: %{enabled: true, min_value: 0.0}}
  },
  default_channels: [:log]
}
```

Resolution merges the category default with any per-strategy override
for that `(strategy, category)`. Start from `PolymarketNotify.Controls.default/0`
(every category on, routed to `default_channels`), or seed one entry
per strategy from a catalog:

```elixir
PolymarketNotify.Controls.from_catalog(["surge", %{tag: "ai"}, "my_custom_strat"])
```

### Writing a channel

```elixir
defmodule MyApp.SlackChannel do
  @behaviour PolymarketNotify.Channel

  @impl true
  def deliver(message, opts) do
    url = PolymarketNotify.resolve(Keyword.get(opts, :webhook_url))
    # ... POST message, return :ok or {:error, reason}
    :ok
  end
end
```

## License

MIT. See [LICENSE](LICENSE).
