Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

currency-notification

A small Go daemon that polls exchange rates, evaluates your personal rules, and sends a Telegram alert when it's a good time to convert.

It uses ExchangeRate-API, whose free tier publishes daily rates derived from ECB/central-bank reference data. That makes this a good tool for deciding when to move money on a horizon of days — "tell me if USD/RUB goes above 95" — and a poor tool for intraday trading: there is no tick data, no bid/ask, and updates land roughly once a day.


How it works

Each poll fetches every quote for your base currency in one API call (the Standard endpoint), stores the observations in SQLite, then evaluates your rules against the current rate plus history. When a rule fires it sends one Telegram message and starts a cooldown so a still-true rule doesn't alert on every poll.

poll → store → evaluate rules (with cooldown) → notify

Setup

1. Get an ExchangeRate-API key

Sign up at https://www.exchangerate-api.com/ and copy your API key from the dashboard. The free tier is enough (see Limitations).

2. Create a Telegram bot and find your chat id

  1. Message @BotFather, send /newbot, and copy the bot token it gives you.
  2. Send any message to your new bot, then open https://api.telegram.org/bot<TOKEN>/getUpdates in a browser and read the chat.id from the JSON. For a group, add the bot to the group and use the negative group id (e.g. -1001234567890).

3. Create your config

cp config.example.yaml config.yaml
$EDITOR config.yaml

config.yaml is gitignored — only config.example.yaml (placeholders) is committed. Keep real secrets out of git.

You can put the API key and bot token in the file, or leave the placeholders and supply them via environment variables (env wins over the file):

export EXCHANGERATE_API_KEY=your-key
export TELEGRAM_BOT_TOKEN=123456:your-bot-token

Configuration reference

api_base_url: https://v6.exchangerate-api.com/v6
api_key: YOUR_EXCHANGERATE_API_KEY   # or env EXCHANGERATE_API_KEY
base_currency: USD                   # the base polled from the provider

rules:
  - pair: USD/RUB
    kind: above
    params: { threshold: 95 }
    cooldown: 24h

telegram:
  bot_token: YOUR_TELEGRAM_BOT_TOKEN  # or env TELEGRAM_BOT_TOKEN
  chat_id: "123456789"

db:
  path: currency-notification.db      # SQLite file; defaults to this if omitted

poll:
  interval: 6h                        # fallback cadence (see scheduling below)

base_currency — the single base fetched each poll. One Standard-endpoint call returns every quote on that base, and each rule's pair is derived from that map. Rules do not have to share the base: a pair like EUR/USD (inverse) or AZN/RUB (cross-rate) is computed from the relevant base quotes — as long as both currencies appear in the response. See Design notes.

rules[] — each rule is a pair (BASE/QUOTE), a kind, kind-specific params, and a cooldown:

kind fires when… params
above current value > threshold threshold
below current value < threshold threshold
cross value moves across threshold vs. the last observation (either direction) threshold
change value moves by ≥ percent over the trailing window percent, window_days

cross and change need history, which accrues automatically as the daemon runs. Comparisons are strict, so a value exactly on the threshold does not fire.

cooldown — after a rule fires, re-alerts are suppressed for this long (Go duration: 24h, 12h, 90m). Last-fired state is persisted in SQLite, so cooldown survives restarts.

poll.interval — the fallback poll cadence. Normally the scheduler polls just after the provider's published next-update time; this interval is only used when that time is unavailable or already past.


Running

Build:

go build -o bin/currency-notification ./cmd/currency-notification

One-shot (cron)

--once runs a single poll+evaluate and exits 0 on success, non-zero on error. Good for cron, since the free tier only updates once a day:

# every day at 08:15, poll and alert
15 8 * * *  EXCHANGERATE_API_KEY=… TELEGRAM_BOT_TOKEN=… /opt/currency-notification/bin/currency-notification --once --config /opt/currency-notification/config.yaml

Long-lived daemon

With no --once, it runs a loop: poll, then sleep until the next scheduled poll, exiting cleanly on SIGINT/SIGTERM.

systemd (/etc/systemd/system/currency-notification.service):

[Unit]
Description=currency-notification FX alerter
After=network-online.target

[Service]
WorkingDirectory=/opt/currency-notification
Environment=EXCHANGERATE_API_KEY=your-key
Environment=TELEGRAM_BOT_TOKEN=123456:your-bot-token
ExecStart=/opt/currency-notification/bin/currency-notification --config /opt/currency-notification/config.yaml
Restart=on-failure

[Install]
WantedBy=multi-user.target

Docker — mount the real config and pass secrets as env; persist the SQLite file on a volume so cooldown state survives restarts:

docker build -t currency-notification .
docker run -d --name currency-notification \
  -e EXCHANGERATE_API_KEY=your-key \
  -e TELEGRAM_BOT_TOKEN=123456:your-bot-token \
  -v "$PWD/config.yaml:/app/config.yaml:ro" \
  -v currency-notification-data:/app \
  currency-notification

Design notes

  • RateProvider is a port. The rule engine depends on an interface, not on ExchangeRate-API. Swapping providers (or using the in-memory mock in tests) needs no change to domain/app.
  • Quota-aware polling. The response carries time_next_update_unix; the scheduler waits until then (plus a few-minute buffer, so the new rate is actually published before we fetch) instead of polling blindly. One Standard call covers all pairs on the base — a handful of calls a day, far under quota.
  • Cross-rate derivation. The provider returns only base→quote rates, but rules can name any pair: DeriveRate computes FROM/TO = (base→TO) / (base→FROM), which yields direct pairs, inverses (EUR/USD from a USD base), and cross-rates (AZN/RUB = USD/RUB ÷ USD/AZN) from that single call. The derived rate for each rule's pair is persisted, so history-based rules (cross/change) work on cross-rates too.
  • Cooldown / hysteresis. After firing, a rule is suppressed for its cooldown window; cross only alerts on the crossing edge, not while it stays across. Last-fired state lives in SQLite, so restarts don't cause a re-alert storm.
  • Injectable Clock. domain and app never call time.Now() — time is a port. The rule engine and cooldown logic are fully deterministic under test.

Limitations

  • Daily, not intraday. The free tier updates about once per day from ECB reference rates. Polling more often just re-reads the same numbers. This is a "when to convert over days" tool, not a trading feed — no bid/ask, no ticks.
  • USD/AZN barely moves. The Azerbaijani manat is effectively pegged to the US dollar (~1.70), so above/below/change rules on USD/AZN will rarely, if ever, fire. Put your rules where there's real movement — USD/RUB, EUR/USD, or the derived cross-rate AZN/RUB (which tracks RUB, not the peg).

Development

make test   # go test ./... -race
make lint   # golangci-lint run
make vet    # go vet ./...
make run    # go run ./cmd/currency-notification --config config.yaml

Default tests use in-memory mock adapters and an injected clock — no network and no real API key required.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages