Packages
polymarket_notify
0.1.0
Notification controls (per-category x per-strategy) plus pluggable channels (Telegram/Discord/webhook/log) for Elixir trading bots. Best-effort delivery, injectable HTTP, no processes or database.
Current section
Files
Jump to
Current section
Files
lib/polymarket_notify.ex
defmodule PolymarketNotify do
@moduledoc """
Notification **controls + channels** for the Polymarket bot.
Two layers:
* **Controls** (`PolymarketNotify.Controls`) — the core: *what* the
user wants to see and *where*, as per-**category** defaults
(`:fills`, `:resolutions`, `:errors`, `:daily_summary`, …) with
per-**strategy** overrides. So you can route `fills` for the AI
strategy to Telegram while muting `fills` for the surge bots, etc.
* **Channels** (`PolymarketNotify.Channel`) — pluggable delivery:
`Telegram`, `Discord`, `Webhook`, `Log`. A channel just ships text.
Strategy is identified by its **tag** (any string) — built-in or a
user-written strategy alike — so custom strategies get their own
notification settings with no code change here.
`notify/3` is **best-effort**: it consults the controls, and if the
(category, strategy, value) passes, fans the message out to that
category's channels. Nothing configured / not allowed ⇒ silent
no-op. It never raises into the caller.
Config:
config :polymarket_notify,
channels: %{
telegram: {PolymarketNotify.Telegram, [bot_token: {:system, "TELEGRAM_BOT_TOKEN"}, chat_id: ...]},
log: {PolymarketNotify.Log, []}
},
controls: PolymarketNotify.Controls.default()
"""
alias PolymarketNotify.Controls
require Logger
@doc """
Deliver `message` for `category`. Options:
* `:strategy` — strategy tag (for per-strategy overrides).
* `:value` — magnitude (e.g. fill notional, P&L) for threshold filters.
* `:controls` / `:channels` — inject (tests); else app config / defaults.
Returns `:ok` (always; best-effort).
"""
@spec notify(atom(), String.t(), keyword()) :: :ok
def notify(category, message, opts \\ []) when is_atom(category) and is_binary(message) do
controls = Keyword.get(opts, :controls) || configured_controls()
channels = Keyword.get(opts, :channels) || configured_channels()
strategy = Keyword.get(opts, :strategy)
value = Keyword.get(opts, :value)
if Controls.allow?(controls, category, strategy, value) do
controls
|> Controls.channels_for(category, strategy)
|> Enum.each(&deliver(&1, channels, message))
end
:ok
rescue
e ->
Logger.debug(fn -> "PolymarketNotify: #{Exception.message(e)}" end)
:ok
catch
kind, reason ->
Logger.debug(fn -> "PolymarketNotify: #{inspect(kind)} #{inspect(reason)}" end)
:ok
end
@doc "Resolve a literal, `{:system, \"VAR\"}` (env), or `nil`. Channels use this for secrets."
@spec resolve(term()) :: term()
def resolve({:system, var}) when is_binary(var), do: System.get_env(var)
def resolve(value), do: value
defp configured_controls,
do: Application.get_env(:polymarket_notify, :controls) || Controls.default()
defp configured_channels,
do: Application.get_env(:polymarket_notify, :channels, %{log: {PolymarketNotify.Log, []}})
defp deliver(channel_name, channels, message) do
case Map.get(channels, channel_name) do
{mod, opts} when is_atom(mod) ->
try do
mod.deliver(message, opts)
rescue
_ -> :ok
catch
_, _ -> :ok
end
_ ->
:ok
end
end
end