Telegram Alerts for Trading Bots: A Notification System Design
TECH INSIGHT / EXPLAINED
A notification path should keep urgent failures visible without blocking the trading engine or creating an unbounded number of background tasks. Treat Telegram as a delivery service with acknowledgements, rate limits and failure states.
Check the API result and protect the token
sendMessage accepts an HTTP request, but success should be checked in both the HTTP status and JSON ok field. The token is part of the request URL; returning a raw network error can leak that URL into logs. Return a safe error and record a redacted diagnostic separately. The excerpt sends plain text intentionally; if HTML mode is enabled, escape all dynamic fields and obey the current message length rules.
// Integration excerpt: use a shared http.Client with a timeout.
func sendTelegram(ctx context.Context, client *http.Client,
token, chatID, text string) error {
if client == nil || token == "" || chatID == "" {
return errors.New("alert channel not configured")
}
form := url.Values{"chat_id": {chatID}, "text": {text}}
endpoint := "https://api.telegram.org/bot" + token + "/sendMessage"
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
endpoint, strings.NewReader(form.Encode()))
if err != nil { return errors.New("invalid alert request") }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil { return errors.New("alert transport failed") }
defer resp.Body.Close()
var result struct { OK bool `json:"ok"` }
if err := json.NewDecoder(io.LimitReader(resp.Body, 65536)).Decode(&result); err != nil {
return errors.New("invalid alert response")
}
if resp.StatusCode != http.StatusOK || !result.OK {
return fmt.Errorf("alert rejected: HTTP %d", resp.StatusCode)
}
return nil
}Use a bounded worker queue
Do not spawn one goroutine for every message. Feed a bounded queue serviced by a small fixed number of workers. Deduplicate repeated incidents by event key and time window, preserve critical incidents durably, and count messages discarded or coalesced during overload. A queue of 100 repeated quote warnings should not bury one failed protective order.
Delivery uncertainty needs an event ID
Handle rate-limit responses using the documented retry_after value, and back off transient transport/server errors within a deadline. Do not retry permanent permission/chat errors forever. An acknowledgement lost after delivery can produce a duplicate on retry; include event ID and observed_at so duplicates are recognizable. Store delivered/failed/pending state. This sender excerpt omits automatic retries by design; the queue owns that policy.
Distinguish submitted, filled and protected
An ENTRY alert at order submission should say submitted, not filled. A position is protected only after the exchange confirms its protective order, and even then execution conditions still matter. Missing critical notifications should be a visible degraded state and may block unattended live startup. Define what action the operator should take, while letting the bot itself continue reconciliation and risk-reduction logic.
The creation of an alert can itself be lost
A process can commit an important order-state change and crash before adding its alert to an in-memory queue. After restart, the state survives but the warning disappears. For critical events, write the business-state change and an outgoing-event record in the same storage transaction. A worker claims pending records with a lease before sending; abandoned leases become candidates for investigation after restart.
This approach assumes the chosen store supports that transaction. Writing to two separate systems does not automatically provide atomicity. A reconciliation job can look for states that require a warning but lack an event record. Store the event ID, original occurrence time, severity, expiry policy, sanitized content, and delivery attempts.
Interpret four distinct delivery outcomes
The Telegram sendMessage documentation specifies a Message object on success. Saving its message ID provides evidence of API acceptance, not evidence that a person read the alert. The following expectations are suitable for synthetic-response tests that send no real messages.
| Observation | State and next action |
|---|---|
| Valid success response with message ID | API acceptance confirmed; stop automatic resends |
| Explicit permanent rejection | Configuration or intervention needed; no endless retries |
| Rate limit with retry_after | Apply the wait within the delivery deadline |
| Timeout or truncated response after submission | Unknown outcome; retain duplicate possibility |
HTTP 200 with an unreadable body does not establish success. Losing the response does not establish that nothing was sent. Collapsing uncertainty into a generic failure makes the retry policy appear simpler than the actual delivery problem.
Choose duplicate tolerance by incident type
For a critical protection failure, missing the alert may cost more than sending it twice. A policy can permit bounded retransmission while retaining the same event ID and original timestamp and indicating possible duplication. For routine status summaries, replacing obsolete retries with a current summary may be preferable. A retry must not manufacture a fresh incident identity.
A durable queue alone cannot guarantee exactly-once delivery when a worker can crash around transmission. Keep overdue records in an expired or operator-review state instead of silently deleting them. Interpret retry_after together with the overall deadline. Reaching that deadline is not permission to bypass the wait or flood the API with immediate requests.
Rehearse queue saturation separately
In a local test, enqueue more routine warnings than the queue can hold and confirm that critical incidents still receive the intended capacity. Demonstrate the selected policy: reserved space by severity, coalescing repeated warnings, or removing expired summaries. Measure the age of the oldest waiting event and counts of discarded or merged records; queue length alone can hide a system that is rapidly dropping everything.
During shutdown, stop intake deliberately, drain for a bounded period, and preserve unfinished records. If the alert path fails, reporting that failure only through the same Telegram path conceals it. Expose degraded delivery in an independent status view or monitoring path. Use a local fake sender for these rehearsals so operational chat rooms do not become the test harness.
Documentation and next steps
Content and examples reviewed: 2026-10-01
댓글
댓글 쓰기