An automated investment system that runs as an AWS Lambda function or a local console application to invest available cash in a Charles Schwab brokerage account according to a predefined allocation strategy.
AutoInvest monitors a Schwab brokerage account and automatically invests available cash into a portfolio of ETFs based on user-defined allocation percentages. The system is designed for resilience and safety, executing trades only when specific conditions are met, such as open market hours and sufficient funds.
Built on clean architecture principles, the solution is decoupled into distinct layers for domain logic, and application services, ensuring high testability, maintainability, and deployment flexibility.
- Automated & Scheduled Investing: Runs on a schedule via AWS EventBridge (or locally) to execute investment strategies without manual intervention.
- Strategic Portfolio Allocation: Invests based on a simple, percentage-based allocation string (e.g., "SPLG:60,SCHG:40").
- Maximized Cash Utilization: The
OrderBook.Generate()method iteratively calculates and places orders to use the maximum available cash, leaving only a small, uninvestable remainder. - Robust Error Handling & Notifications: Sends detailed success, failure, or unfilled orders email notifications via AWS SES using HTML templates.
- Secure Credential Management: All sensitive data (Client ID, Client Secret, Refresh Token) is securely stored and retrieved from AWS Secrets Manager.
- Automated OAuth2 Refresh: Includes a standalone Node.js tool using Puppeteer to handle Schwab's mandatory interactive login for refreshing authentication tokens.
- Dual Deployment Targets: Can be deployed as a serverless AWS Lambda function or run as a local console application that can be set on a schedule using Windows Scheduler.
- Configuration Validation: Performs rigorous startup checks via
ConfigurationValidatorto validate all settings, preventing runtime errors from misconfiguration.
The solution is logically divided into several projects, each with a distinct responsibility, following the principles of Clean Architecture.
graph TB
subgraph "Execution Environments"
direction LR
Lambda["src/AutoInvest (AWS Lambda)"]
Local["AutoInvestLocal (Console)"]
Auth["AutoInvest.AuthAutomation (Node.js)"]
end
subgraph "Application & Domain Layers"
direction TB
Services["AutoInvest.Services"]
Core["AutoInvest.Core"]
end
subgraph "External Services"
direction LR
Schwab["Charles Schwab API"]
Secrets["AWS Secrets Manager"]
SES["AWS SES"]
end
Lambda --> Services
Local --> Services
Auth -.-> Secrets
Auth -.-> SES
Services --> Core
Services --> Schwab
Services --> Secrets
Services --> SES
| Project | Description |
|---|---|
| src/AutoInvest | AWS Lambda entry point with Functions.cs and Startup.cs for dependency injection |
| AutoInvestLocal | Console application for local development, testing, or running on a schedule using Windows Scheduler |
| AutoInvest.Services | Application services including InvestmentOrchestrator, Schwab API clients, and AWS integrations |
| AutoInvest.Core | Domain logic including OrderBook, InvestmentCalculator, AllocationStrategy, and email builders |
| AutoInvest.AuthAutomation | Node.js/Puppeteer tool for automated Schwab OAuth2 token refresh |
| AutoInvest.Core.Tests | Unit tests for domain logic |
| AutoInvest.Services.Tests | Unit tests for application services |
graph TD
A[Start] --> B{Validate Config};
B --> |Invalid| Z[Exit & Log Error];
B --> |Valid| C[Check Market Status];
C --> |Closed| D[Exit & Log];
C --> |Open| E[Get Account Hash & Details];
E --> F{Cash Balance > 0?};
F --> |No| G[Exit & Log];
F --> |Yes| H[Get Quotes for Allocation Symbols];
H --> I[Generate OrderBook];
I --> J{Any Orders to Place?};
J --> |No| K[Exit & Log];
J --> |Yes| L[Submit All Orders];
L --> M[Check for Unfilled Orders];
M --> N{Any Unfilled?};
N --> |Yes| O[Send Unfilled Orders Email];
N --> |No| P[Get Updated Account Balance];
O --> P;
P --> Q{Any Filled Orders?};
Q --> |Yes| R[Send Success Email];
Q --> |No| S[Log - No Success Email];
R --> Y[End];
S --> Y;
K --> Y;
G --> Y;
D --> Y;
Z --> Y;
Create a secret in AWS Secrets Manager to store all sensitive credentials.
- Secret name: Name for the configuration (referenced by
Aws:SecretsName). - Key/value pairs:
{ "REFRESH_TOKEN": "schwab-refresh-token", // Updated by AutoInvest.AuthAutomation "CLIENT_ID": "schwab-client-id", "CLIENT_SECRET": "schwab-client-secret" }Note: The
REFRESH_TOKENis automatically updated by theAutoInvest.AuthAutomationtool.
The Lambda execution role needs permissions to access:
- AWS Secrets Manager - Read/write access for token management
- AWS SES - Send email permissions for notifications
- CloudWatch Logs - For logging
Configure the application using environment variables (for AWS Lambda) or user secrets (for local development).
{
"Schwab": {
"MarketDataApiUrl": "https://api.schwabapi.com/marketdata/v1",
"TradingApiUrl": "https://api.schwabapi.com/trader/v1",
"AuthorizationUrl": "https://api.schwabapi.com/v1/oauth/authorize",
"TokenUrl": "https://api.schwabapi.com/v1/oauth/token"
},
"Trading": {
"AllocationsString": "VOO:80,VXUS:20"
},
"AWS": {
"Region": "aws-region",
"Profile": "aws-profile-name",
"SecretsName": "aws-secrets-name",
"ToEmail": "to-email@example.com",
"FromEmail": "from-email@example.com"
}
}| Setting | Description |
|---|---|
Schwab:MarketDataApiUrl |
Schwab Market Data API endpoint for quotes and market hours |
Schwab:TradingApiUrl |
Schwab Trading API endpoint for account and order operations |
Schwab:TokenUrl |
OAuth2 token exchange endpoint |
Trading:AllocationsString |
Comma-separated symbol:percentage pairs (must sum to 100) |
Aws:SecretsName |
Name of the secret in AWS Secrets Manager |
Aws:ToEmail |
Recipient email for notifications (must be SES verified) |
Aws:FromEmail |
Sender email for notifications (must be SES verified) |
-
Build, Package, and Deploy:
.\upload_lambda.ps1 -FunctionName "YourFunctionName"
This script builds the project, creates a deployment zip, and uploads it using AWS CLI.
-
Configure Environment Variables in AWS: Set the configuration values listed above in the Lambda function's configuration section.
-
Schedule: Create an AWS EventBridge (CloudWatch Events) rule to trigger the Lambda function on a schedule. A cron expression is recommended to run during market hours.
- Example Cron (Runs at 10:00 AM UTC on Fridays):
cron(0 10 ? * FRI *)
- Example Cron (Runs at 10:00 AM UTC on Fridays):
Run the console application locally for testing:
cd AutoInvestLocal
dotnet runNote: Ensure user secrets are configured with
dotnet user-secrets set "Key" "Value"for local development.
Because Schwab's refresh_token expires, you must run the AutoInvest.AuthAutomation tool once a week to get a new one. This automation uses Puppeteer to simulate a browser login to Schwab and retrieve a new refresh_token, storing it in AWS Secrets Manager. It also sends email notifications if a failure occurs.
- Install Node.js and npm.
- Navigate to the
AutoInvest.AuthAutomationdirectory. - Run
npm install. - Create a
.envfile based on.example.env:
# Schwab Environment Variables
CLIENT_ID={Schwab Client ID}
CLIENT_SECRET={Schwab Client Secret}
REDIRECT_URI={Schwab Redirect URI}
LOGIN_ID={Schwab Login ID}
PASSWORD={Schwab Password}
# AWS Environment Variables
AWS_ACCESS_KEY_ID={AWS Access Key ID}
AWS_SECRET_ACCESS_KEY={AWS Secret Access Key}
AWS_REGION={AWS Region}
SECRET_NAME={AWS Secret Name}
SES_FROM_EMAIL={SES Verified From Email}
SES_TO_EMAIL={SES Verified To Email}
# Puppeteer Environment Variable
BROWSER_EXECUTABLE_PATH={Path to Chrome}When running this the first time, you might need to manually interact with the browser to complete any multi-factor authentication steps; set the headless parameter to false in automationUser.js. The timeout is set to 1 minute 30 seconds to allow for manual interaction on the first run.
📢 When going through the initial setup to choose which accounts the refresh token will be allowed to work with, ONLY SELECT ONE ACCOUNT to auto invest in. This application DOES NOT SUPPORT multiple accounts.
- Run the script from the command line:
node auth.js
- Screenshots are automatically saved to a date-organized folder under
screenshots/for debugging. - Logs are written to the
logs/directory using Winston with daily rotation. - It is recommended to schedule this script to run once a week automatically on a local machine where you can access it directly if MFA intervention is required.