Overview
This application allows users to create alerts for selected stock tickers on the NYSE. When the market price reaches or exceeds the target price, the application sends a notification directly to a Telegram chat via a bot. This way, we cover several serverless services and implement a common app flow to be used as a boilerplate.
The solution uses:
• AWS Lambda (Python) for backend logic:
• Alerts Function: CRUD operations for alerts stored in DynamoDB.
• Price Scanner Function: Scheduled function to query current stock prices via yfinance and send Telegram messages.
• Basic Authorizer: A simple Lambda authorizer to secure API Gateway endpoints.
• Amazon DynamoDB to store alerts.
• Amazon API Gateway to expose REST endpoints.
• AWS SAM for deployment.
• Amazon S3 for hosting the frontend static website.
• Telegram Bot API for push notifications.
Prerequisites
• AWS Account with permissions to deploy Lambda, DynamoDB, API Gateway, SNS, and S3 resources.
• AWS SAM CLI installed and configured.
• Python 3.9 environment.
• Docker (for local SAM testing with container emulation).
• A Telegram Bot created via @botfather with a valid bot token.
• A method to retrieve your Telegram Chat ID (by sending a message to the bot and calling the getUpdates API).
Project Structure
Github repository
You can find this project on this Github repository:
https://github.com/joseapeinado/stocks-alerts-app
Step 1. Develop the Backend Functions
1.1 Alerts Function (alerts.py)
This function implements REST endpoints to list, create, update, and delete alerts in DynamoDB. It also integrates with yfinance to retrieve the current stock price when listing alerts.
backend/handlers/alerts.py
import os
import json
import uuid
from decimal import Decimal
import boto3
from datetime import datetime
import yfinance as yf
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['ALERTS_TABLE'])
sns = boto3.client('sns')
sns_topic_arn = os.environ['SNS_TOPIC_ARN']
def lambda_handler(event, context):
try:
resource = event.get("resource", "")
http_method = event.get("httpMethod", "")
if resource == "/alerts":
if http_method == "GET":
result = table.scan()
items = result.get("Items", [])
for item in items:
item["currentPrice"] = str(get_current_price(item.get("ticker", "")))
return _response(200, items)
elif http_method == "POST":
body = json.loads(event["body"])
alert = {
"alertId": str(uuid.uuid4()),
"ticker": body["ticker"],
"price": str(Decimal(str(body["price"]))),
"createdAt": datetime.utcnow().isoformat(),
"paused": False # Default state: active
}
table.put_item(Item=alert)
alert["currentPrice"] = str(get_current_price(alert["ticker"]))
return _response(201, alert)
elif http_method == "PUT":
# Update price and/or paused state.
body = json.loads(event["body"])
alert_id = body["alertId"]
update_expression = []
expression_attribute_values = {}
if "price" in body:
update_expression.append("price = :p")
expression_attribute_values[":p"] = str(Decimal(str(body["price"])))
if "paused" in body:
update_expression.append("paused = :s")
expression_attribute_values[":s"] = bool(body["paused"])
if not update_expression:
return _response(400, {"error": "No valid fields to update."})
update_expr = "set " + ", ".join(update_expression)
result = table.update_item(
Key={"alertId": alert_id},
UpdateExpression=update_expr,
ExpressionAttributeValues=expression_attribute_values,
ReturnValues="ALL_NEW"
)
updated_item = result.get("Attributes", {})
updated_item["currentPrice"] = str(get_current_price(updated_item.get("ticker", "")))
return _response(200, updated_item)
elif http_method == "DELETE":
alert_id = event["queryStringParameters"].get("alertId")
table.delete_item(Key={"alertId": alert_id})
return _response(200, {"message": "Alert deleted"})
else:
return _response(405, "Method Not Allowed")
else:
return _response(404, "Not Found")
except Exception as e:
return _response(500, {"error": str(e)})
def get_current_price(ticker):
try:
data = yf.Ticker(ticker)
current_price = data.info.get("regularMarketPrice")
return Decimal(str(current_price)) if current_price is not None else Decimal("0")
except Exception:
return Decimal("0")
def _response(status_code, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps(body, default=str)
}
1.2 Price Scanner Function (priceScanner.py)
This function is scheduled to run periodically. It scans the AlertsTable, checks the current stock price using yfinance, and if the price meets or exceeds the target, it sends a message via Telegram. It also skips alerts that are paused.
backend/handlers/priceScanner.py
import os
import json
from decimal import Decimal
import boto3
import yfinance as yf
from datetime import datetime
import logging
import requests
# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['ALERTS_TABLE'])
# Telegram Bot credentials from environment variables
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
def lambda_handler(event, context):
result = table.scan()
alerts = result.get("Items", [])
triggered_alerts = []
now = datetime.utcnow()
for alert in alerts:
# Skip paused alerts
if alert.get("paused", False):
continue
ticker = alert.get("ticker")
target_price = Decimal(alert.get("price"))
current_price = get_current_price(ticker)
# Trigger if the current price meets or exceeds the target
if current_price >= target_price:
message = (
f"Alert: {ticker} current price ${current_price} meets/exceeds your target of ${target_price}."
)
send_telegram_message(message)
triggered_alerts.append(alert)
logger.info("Triggered Alert: %s", json.dumps({
"alertId": alert.get("alertId"),
"ticker": ticker,
"targetPrice": str(target_price),
"currentPrice": str(current_price),
"triggeredAt": now.isoformat()
}))
return {
"statusCode": 200,
"body": json.dumps({
"message": "Price scan completed",
"triggeredAlerts": triggered_alerts
}, default=str)
}
def get_current_price(ticker):
try:
data = yf.Ticker(ticker)
current_price = data.info.get("regularMarketPrice")
return Decimal(str(current_price)) if current_price is not None else Decimal("0")
except Exception as e:
logger.error("Error retrieving price for %s: %s", ticker, e)
return Decimal("0")
def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
logger.error("Telegram bot token or chat id not configured.")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {
"chat_id": TELEGRAM_CHAT_ID,
"text": message
}
try:
response = requests.get(url, params=payload)
logger.info("Telegram response: %s", response.text)
except Exception as e:
logger.error("Error sending Telegram message: %s", e)
Note: To address a cache warning from yfinance, add the following after importing yfinance:
yf.set_tz_cache_location('/tmp/py-yfinance')
1.3 Basic Authorizer (basicAuthorizer.py)
This minimalistic authorizer validates the incoming Bearer token. In production, store sensitive values securely.
backend/handlers/basicAuthorizer.py
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("Received event: %s", json.dumps(event))
try:
token = event.get("authorizationToken", "")
expected_token = "secret-token-123" # Replace or secure this token as needed
effect = "Allow" if token == f"Bearer {expected_token}" else "Deny"
auth_response = {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": effect,
"Resource": event.get("methodArn")
}
]
}
}
logger.info("Authorization response: %s", json.dumps(auth_response))
return auth_response
except Exception as e:
logger.error("Error in authorizer", exc_info=True)
raise e
Requirements file
Please create a requirements.txt file to manage Python dependencies, with the following modules:
yfinance
requests
Step 2. Develop the Frontend
2.1 HTML (index.html)
This basic HTML page hosts a form for creating alerts and a table to display alerts with actions to update, delete, or toggle pause.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stock Price Alerts</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Stock Price Alerts</h1>
<form id="alertForm">
<input type="text" id="ticker" placeholder="Ticker" required>
<span id="currentPriceDisplay"></span>
<input type="number" id="price" placeholder="Target Price" step="0.01" required>
<button type="submit">Add Alert</button>
</form>
<h2>Your Alerts</h2>
<table id="alertsTable">
<thead>
<tr>
<th>Ticker</th>
<th>Target Price</th>
<th>Current Price</th>
<th>Created At</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="alertsTableBody"></tbody>
</table>
</div>
<script src="main.js"></script>
</body>
</html>
2.2 JavaScript (main.js)
The JavaScript handles user interactions: fetching the current price on ticker blur, creating alerts, and updating/deleting/toggling alert state via API calls.
const apiUrl = 'https://<YOUR_API_GATEWAY_ID>.execute-api.<region>.amazonaws.com/Prod/alerts';
const priceUrl = 'https://<YOUR_API_GATEWAY_ID>.execute-api.<region>.amazonaws.com/Prod/price';
const authHeader = 'Bearer secret-token-123';
document.addEventListener('DOMContentLoaded', () => {
loadAlerts();
// When the ticker field loses focus, show the current price.
const tickerInput = document.getElementById('ticker');
tickerInput.addEventListener('blur', async () => {
const ticker = tickerInput.value.trim();
const display = document.getElementById('currentPriceDisplay');
if (ticker) {
try {
const response = await fetch(`${priceUrl}?ticker=${encodeURIComponent(ticker)}`, {
method: 'GET',
headers: { 'Authorization': authHeader }
});
const data = await response.json();
if (response.ok && data.currentPrice) {
display.textContent = `Current Price: $${data.currentPrice}`;
} else {
display.textContent = 'Current Price: N/A';
}
} catch (err) {
display.textContent = 'Current Price: Error';
}
} else {
display.textContent = '';
}
});
});
document.getElementById('alertForm').addEventListener('submit', async (e) => {
e.preventDefault();
const ticker = document.getElementById('ticker').value;
const price = parseFloat(document.getElementById('price').value);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader
},
body: JSON.stringify({ ticker, price })
});
if (response.ok) {
loadAlerts();
e.target.reset();
document.getElementById('currentPriceDisplay').textContent = '';
}
});
async function loadAlerts() {
const response = await fetch(apiUrl, {
method: 'GET',
headers: { 'Authorization': authHeader }
});
const alerts = await response.json();
const tableBody = document.getElementById('alertsTableBody');
tableBody.innerHTML = '';
alerts.forEach(alert => {
const tr = document.createElement('tr');
// Ticker cell
const tdTicker = document.createElement('td');
tdTicker.textContent = alert.ticker;
tr.appendChild(tdTicker);
// Target Price cell
const tdPrice = document.createElement('td');
tdPrice.textContent = alert.price;
tr.appendChild(tdPrice);
// Current Price cell
const tdCurrentPrice = document.createElement('td');
tdCurrentPrice.textContent = alert.currentPrice || "N/A";
tr.appendChild(tdCurrentPrice);
// Created At cell
const tdCreatedAt = document.createElement('td');
tdCreatedAt.textContent = alert.createdAt;
tr.appendChild(tdCreatedAt);
// State cell (Active or Paused)
const tdState = document.createElement('td');
tdState.textContent = alert.paused ? "Paused" : "Active";
tr.appendChild(tdState);
// Actions cell with three buttons: Update Price, Delete, Toggle Pause
const tdActions = document.createElement('td');
tdActions.innerHTML = `
<button class="icon-button" title="Update Price" onclick='updateAlert(${JSON.stringify(alert)})'>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="currentColor"/>
</svg>
</button>
<button class="icon-button" title="Delete" onclick='deleteAlert("${alert.alertId}")'>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-4.5l-1-1z" fill="currentColor"/>
</svg>
</button>
<button class="icon-button" title="Toggle Pause" onclick='toggleAlertState(${JSON.stringify(alert)})'>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path d="M12 2a10 10 0 100 20 10 10 0 000-20zm1 14h-2v-4h2v4zm0-6h-2V7h2v3z" fill="currentColor"/>
</svg>
</button>
`;
tr.appendChild(tdActions);
tableBody.appendChild(tr);
});
}
async function updateAlert(alert) {
const newPrice = prompt('Enter new price', alert.price);
if (newPrice) {
const response = await fetch(apiUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader
},
body: JSON.stringify({
alertId: alert.alertId,
ticker: alert.ticker,
price: parseFloat(newPrice)
})
});
if (response.ok) {
loadAlerts();
} else {
alert('Update failed.');
}
}
}
async function deleteAlert(alertId) {
if (confirm('Are you sure you want to delete this alert?')) {
const response = await fetch(`${apiUrl}?alertId=${alertId}`, {
method: 'DELETE',
headers: { 'Authorization': authHeader }
});
if (response.ok) {
loadAlerts();
} else {
alert('Deletion failed.');
}
}
}
async function toggleAlertState(alert) {
const newPaused = !alert.paused;
const response = await fetch(apiUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader
},
body: JSON.stringify({
alertId: alert.alertId,
ticker: alert.ticker,
price: alert.price,
paused: newPaused
})
});
if (response.ok) {
loadAlerts();
} else {
alert('Toggle failed.');
}
}
2.3 CSS (styles.css)
Ensure your icons and table display correctly:
/* Table styling */
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
/* Icon button styling */
.icon-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
margin: 0 2px;
color: #000; /* Ensure currentColor is visible */
}
.icon-button svg {
fill: currentColor;
}
.icon-button:hover svg {
fill: #0073e6;
}
/* Current price display next to ticker input */
#currentPriceDisplay {
margin-left: 10px;
font-weight: bold;
color: #333;
}
Step 3. SAM Template
Below is the final template.yaml for the application. It provisions all resources (DynamoDB, SNS, Lambda functions, API Gateway, S3 bucket for frontend) and configures environment variables and policies.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Minimal Stock Price Alerts WebApp with Python Lambdas, CORS, S3 static website, and Outputs
Parameters:
Environment:
Type: String
Default: Prod
AllowedValues:
- Prod
- Dev
Description: Environment type
Globals:
Function:
Timeout: 10
Resources:
AlertsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: AlertsTable
AttributeDefinitions:
- AttributeName: alertId
AttributeType: S
KeySchema:
- AttributeName: alertId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
SNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: StockAlertsTopic
AlertsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: backend/handlers/
Handler: alerts.lambda_handler
Runtime: python3.9
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref AlertsTable
Environment:
Variables:
ALERTS_TABLE: !Ref AlertsTable
SNS_TOPIC_ARN: !Ref SNSTopic
Events:
AlertsAPI:
Type: Api
Properties:
Path: /alerts
Method: any
RestApiId: !Ref AlertsApi
PriceLookup:
Type: Api
Properties:
Path: /price
Method: GET
RestApiId: !Ref AlertsApi
PriceScannerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: backend/handlers/
Handler: priceScanner.lambda_handler
Runtime: python3.9
Timeout: 30 # Increased timeout for external API calls
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref AlertsTable
- SNSPublishMessagePolicy:
TopicName: StockAlertsTopic
Environment:
Variables:
ALERTS_TABLE: !Ref AlertsTable
SNS_TOPIC_ARN: !GetAtt SNSTopic.TopicArn
TELEGRAM_CHAT_ID: "REDACTED"
TELEGRAM_BOT_TOKEN: "REDACTED"
Events:
PriceScannerSchedule:
Type: Schedule
Properties:
Schedule: rate(10 minutes)
EmailSubscription1:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref SNSTopic
Protocol: email
Endpoint: "joseapeinado@gmail.com"
BasicAuthAuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: backend/handlers/
Handler: basicAuthorizer.lambda_handler
Runtime: python3.9
MemorySize: 128
Timeout: 5
AlertsApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors:
AllowOrigin: "'*'"
AllowHeaders: "'Content-Type,Authorization'"
AllowMethods: "'OPTIONS,GET,POST,PUT,DELETE'"
Auth:
DefaultAuthorizer: BasicAuthorizer
AddDefaultAuthorizerToCorsPreflight: false
Authorizers:
BasicAuthorizer:
FunctionArn: !GetAtt BasicAuthAuthorizerFunction.Arn
Identity:
ReauthorizeEvery: 0
Headers:
- Authorization
StaticSiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: stocks-alerts-app-staticsitebucket-12345678
WebsiteConfiguration:
IndexDocument: index.html
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
StaticSiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref StaticSiteBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: PublicReadGetObject
Effect: Allow
Principal: "*"
Action: s3:GetObject
Resource: !Sub "${StaticSiteBucket.Arn}/*"
Outputs:
ApiUrl:
Description: "API Gateway endpoint URL for the Prod stage"
Value: !Sub "https://${AlertsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/alerts"
AlertsTableName:
Description: "DynamoDB Alerts Table Name"
Value: !Ref AlertsTable
SNSTopicARN:
Description: "SNS Topic ARN"
Value: !Ref SNSTopic
StaticSiteURL:
Description: "URL for static website hosted on S3"
Value: !GetAtt StaticSiteBucket.WebsiteURL
Step 4. Build, Deploy, and Test
- Build the Application:
sam build
Building codeuri: /Users/josepeinado/jose/projects/serverless/stocks-alerts-app/backend/handlers runtime: python3.9 architecture: x86_64 functions: AlertsFunction, PriceScannerFunction, BasicAuthAuthorizerFunction
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
- Deploy the Application: You can use a configuration file (samconfig.toml): samconfig.toml
version = 0.1
[default.deploy.parameters]
stack_name = "stocks-alerts-app"
resolve_s3 = true
s3_prefix = "stocks-alerts-app"
region = "us-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []
sam deploy
- Deploy the Frontend: Sync the frontend/ folder to your S3 bucket:
aws s3 sync frontend/ s3://stocks-alerts-app-staticsitebucket-12345678 --delete
- Testing: • Use your browser to load the static site URL. • Create alerts for specific tickers.
• Verify via the Telegram chat that alerts are sent when the price condition is met. At this moment, I chose to trigger the alert when the target price is equal to or above the current price, so please adjust this to avoid getting alerts every time :).
• Use the table to update, delete, or toggle pause state on alerts.
Rationale & Considerations
• Serverless Architecture:
Leveraging AWS Lambda, API Gateway, and DynamoDB allows for a highly scalable and cost-effective solution. AWS SAM streamlines deployment and resource management.
• Python & yfinance:
Python provides a concise syntax and powerful libraries. The yfinance library lets us retrieve real-time market data without a paid API, though note it’s unofficial.
• Telegram Integration:
Using Telegram Bot API enables real-time push notifications. This is particularly useful for immediate alerting without relying on email (which might be slower or filtered).
• Security:
The Basic Authorizer secures API endpoints. In production, consider using a more robust authentication mechanism and store sensitive tokens securely (e.g., AWS Secrets Manager).
• Extensibility:
The design supports easy extension. For example, additional endpoints, richer alert criteria, or integration with other notification systems can be added with minimal changes.
Top comments (0)