Skip to content

Feat/m4 telegram bot#28

Merged
PCBZ merged 29 commits into
mainfrom
feat/m4-telegram-bot
May 26, 2026
Merged

Feat/m4 telegram bot#28
PCBZ merged 29 commits into
mainfrom
feat/m4-telegram-bot

Conversation

@PCBZ
Copy link
Copy Markdown
Owner

@PCBZ PCBZ commented May 25, 2026

No description provided.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “trade-compass” Telegram bot deployed on Cloud Run, with scheduled push notifications, and extends the existing API to support LLM model selection and decision history.

Changes:

  • Adds a new terraform/bot stack to build/deploy the bot to Cloud Run and create Cloud Scheduler push jobs, plus deploy script automation to wire env vars and register Telegram webhooks.
  • Introduces a new bot/ Python service (FastAPI + python-telegram-bot + LangGraph) that calls FMP + OpenRouter and persists decisions to the existing API.
  • Updates API routing (no trailing slash redirects), adds /decisions listing, and extends preferences with llm_model.

Reviewed changes

Copilot reviewed 40 out of 46 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
terraform/deploy.sh Extends deployment automation to deploy the bot, update bot/.env, and register Telegram webhook.
terraform/cloud_run/main.tf Switches image build to Cloud Build and adds project-level IAM grants + propagation delay.
terraform/bootstrap/main.tf Enables additional GCP APIs (Cloud Build, Cloud Scheduler).
terraform/atlas/main.tf Updates Atlas IP access list rules for Cloud Run egress.
terraform/bot/backend.tf Adds GCS backend config for the bot Terraform state.
terraform/bot/variables.tf Defines bot Terraform inputs (tokens/keys, project/region, tfstate bucket).
terraform/bot/main.tf Creates bot Cloud Run service, Secret Manager secrets, public invoker binding, and Cloud Scheduler push jobs.
terraform/bot/outputs.tf Exposes the bot Cloud Run URL for webhook registration/testing.
bot/main.py Adds FastAPI app entrypoint with /health, /webhook, and /push.
bot/Dockerfile Defines container image build for the bot service.
bot/requirements.txt Adds runtime Python dependencies for the bot service.
bot/requirements-dev.txt Adds pytest tooling dependencies for the bot.
bot/pytest.ini Configures pytest asyncio mode.
bot/.dockerignore Prevents secrets/tests/dev files from being included in the container build context.
bot/.env.example Documents required runtime env vars for local/integration usage.
bot/config.json Adds configurable OpenRouter model list and default selection.
bot/config.py Loads config.json and exposes model helpers/validation.
bot/state.py Defines the shared analysis workflow state model (TypedDicts).
bot/graph/workflow.py Defines LangGraph workflow topology for single-stock and portfolio analysis.
bot/tools/llm.py Adds OpenRouter-backed LLM client helper via LangChain.
bot/tools/market_data.py Adds FMP async client utilities for quote/profile/financials/news/ratings/scores.
bot/tools/portfolio_api.py Adds async REST client for calling the deployed trade-compass API.
bot/tools/prompt.py Builds the structured decision prompt from analysis outputs.
bot/tg/bot.py Implements Telegram webhook router and Application wiring.
bot/tg/handlers.py Implements /decide, /portfolio, /model, /help Telegram command handlers.
bot/tg/push.py Implements /push endpoint triggered by Cloud Scheduler to send Telegram summaries.
bot/tests/test_api.py Adds integration tests for deployed API connectivity/auth and endpoints.
bot/tests/test_fmp.py Adds integration tests for FMP connectivity and response shapes.
bot/tests/test_openrouter.py Adds integration tests for OpenRouter connectivity and structured outputs.
bot/tests/test_push.py Adds integration test for deployed bot /push endpoint.
api/src/main.py Disables redirect slashes to standardize route shapes.
api/src/models.py Adds llm_model to preferences model.
api/src/routers/holdings.py Changes holdings routes to "" to match redirect_slashes=False.
api/src/routers/preferences.py Changes preferences routes to "" to match redirect_slashes=False.
api/src/routers/decisions.py Adds GET "" list endpoint and changes POST route to "".

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread terraform/deploy.sh Outdated
Comment thread terraform/deploy.sh
Comment thread terraform/atlas/main.tf Outdated
Comment thread terraform/bot/main.tf
Comment thread terraform/bot/main.tf
Comment thread bot/src/tools/market_data.py
Comment thread bot/src/tools/market_data.py
Comment thread terraform/cloud_run/main.tf
Comment thread api/src/models.py
Comment thread terraform/deploy.sh Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 52 changed files in this pull request and generated 13 comments.

Comment thread terraform/deploy.sh
Comment on lines +53 to +57
set_env() {
local key="$1" val="$2"
local tmp
tmp="$(mktemp)"
if grep -q "^${key}=" "${ENV_FILE}" 2>/dev/null; then
Comment thread terraform/deploy.sh
Comment on lines +96 to +106
# Wait for bot Cloud Run to be healthy before registering webhook
echo " Waiting for bot service to be healthy..."
for i in $(seq 1 12); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BOT_URL}/health" 2>/dev/null || echo "000")
if [ "${STATUS}" = "200" ]; then
echo " Bot service is healthy (attempt ${i})"
break
fi
echo " Attempt ${i}/12: status=${STATUS}, retrying in 10s..."
sleep 10
done
Comment thread terraform/deploy.sh
Comment on lines +108 to +113
# Token is passed in the URL path (Telegram API requirement), but we avoid
# echoing the full URL to logs to reduce accidental token exposure.
WEBHOOK_RESP=$(curl -s \
--url "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
--data-urlencode "url=${BOT_URL}/webhook")
if echo "${WEBHOOK_RESP}" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('ok') else 1)"; then
Comment on lines +42 to +46
resource "google_project_iam_member" "cloudbuild_storage" {
project = var.gcp_project_id
role = "roles/storage.admin"
member = local.cloudbuild_sa
}
Comment thread terraform/atlas/main.tf
Comment on lines +62 to +68
# M0 free tier does not support VPC peering or private endpoints.
# Real security is the credentials stored in Secret Manager.
resource "mongodbatlas_project_ip_access_list" "cloud_run" {
project_id = mongodbatlas_project.trade_compass.id
cidr_block = "0.0.0.0/0"
comment = "Cloud Run egress — M0 does not support VPC peering; auth via Secret Manager"
}
Comment thread bot/src/tg/push.py
Comment on lines +40 to +53
@push_router.post("/push")
async def push(request: Request, body: PushRequest) -> dict:
"""Triggered by Cloud Scheduler. Runs portfolio analysis and sends to Telegram."""
if body.type not in _PUSH_TYPES:
raise HTTPException(
status_code=400,
detail=f"Unknown push type '{body.type}'. Valid: {list(_PUSH_TYPES)}",
)

chat_id = os.environ.get("TELEGRAM_CHAT_ID")
token = os.environ.get("TELEGRAM_BOT_TOKEN")
if not chat_id or not token:
raise HTTPException(status_code=500, detail="TELEGRAM_CHAT_ID or TELEGRAM_BOT_TOKEN not set")

Comment thread terraform/bot/main.tf
Comment on lines +222 to +264
# ── Allow unauthenticated (Telegram webhook needs public access) ──────────────
resource "google_cloud_run_v2_service_iam_member" "public" {
name = google_cloud_run_v2_service.bot.name
location = var.region
role = "roles/run.invoker"
member = "allUsers"
}

# ── Cloud Scheduler: 5 daily push notifications ───────────────────────────────
# All times in UTC (ET = UTC-4 in summer, UTC-5 in winter; using UTC-4 / EDT)
# pre_market 09:25 ET → 13:25 UTC
# morning 11:00 ET → 15:00 UTC
# noon 12:30 ET → 16:30 UTC
# afternoon 14:30 ET → 18:30 UTC
# post_market 16:05 ET → 20:05 UTC

locals {
push_schedules = {
pre_market = "25 13 * * 1-5"
morning = "0 15 * * 1-5"
noon = "30 16 * * 1-5"
afternoon = "30 18 * * 1-5"
post_market = "5 20 * * 1-5"
}
}

resource "google_cloud_scheduler_job" "push" {
for_each = local.push_schedules

name = "trade-compass-push-${each.key}"
description = "trade-compass bot ${each.key} push"
schedule = each.value
time_zone = "UTC"
attempt_deadline = "180s"

http_target {
http_method = "POST"
uri = "${google_cloud_run_v2_service.bot.uri}/push"
body = base64encode(jsonencode({ type = each.key }))
headers = {
"Content-Type" = "application/json"
}
}
Comment thread bot/src/tg/bot.py
Comment on lines +70 to +76
@webhook_router.post("/webhook")
async def webhook(request: Request) -> Response:
"""Receive Telegram update and dispatch to handlers."""
data = await request.json()
update = Update.de_json(data, application.bot)
await application.process_update(update)
return Response(status_code=200)
Comment thread bot/src/tg/bot.py
Comment on lines +35 to +38
def build_application() -> Application:
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
app = Application.builder().token(token).build()

Comment on lines +16 to +24
@router.get(
"",
responses={200: {"content": {"application/json": {"example": [_example]}}}},
)
async def list_decisions():
"""Return all decisions, newest first."""
db = get_db()
return await db.decisions.find({}, {"_id": 0}).sort("created_at", -1).to_list(None)

@PCBZ PCBZ merged commit 835ba1d into main May 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment