Algo Trading Bot
Build a paper-only BTC/USD RSI trading dashboard with Python and Alpaca.
Introduction
30 Second Summary
Fast-moving prices can turn a simple trading decision into a rushed guess. A clear set of rules gives each decision the same test.
In this project, you will build a local desktop bot that connects to an Alpaca Trading API paper account. It turns completed BTC/USD candles into one-time RSI crossover signals for simulated orders.
What You'll Build
You'll watch a dark trading dashboard react to live BTC/USD data as a simulated test order updates your paper position.
By the end of this project, you'll have:
- A deterministic signal demo that reveals two repeated BUY decisions from a threshold-only rule.
- A closed-candle crossover strategy that issues one entry signal at the crossing point before returning HOLD.
- A paper-trading dashboard where you can monitor price, RSI, position, unrealized P&L, equity, charts, and activity. You can send a manual simulated order. Bot executions appear in trades.csv.
- Secret Mission: Reconfigure the same crossover engine to use 30 and 70 RSI bands without editing Python.
Are there any prerequisites?
No prior trading or Python experience is required. Step 1 sets up every tool plus your free Alpaca paper account.
Before We Start
This opening checkpoint locks in the paper-only BTC/USD bot you are about to build. It keeps your focus on learning execution mechanics without risking real funds.
Set Up Python, VS Code, and Paper Trading
Your paper-only bot needs a predictable local foundation before any trading code can run. An isolated Python environment keeps this project's packages separate from everything else on your computer.
This step also verifies Tkinter for the desktop dashboard. You will prepare Visual Studio Code before generating separate credentials for an Alpaca Paper Trading account.
In this step, get ready to:
- Install Python 3.14.7 with Visual Studio Code.
- Create an isolated virtual environment with the project's pinned dependencies.
- Prepare the project files with private Alpaca paper credentials.
Install Python and Visual Studio Code
The project uses Python 3.14.7 so every pinned package runs against the same interpreter. Start by checking whether that version is already available.
- Open PowerShell on Windows or Terminal on macOS.
- Check your installed Python version using the command for your operating system below:
macOS
- Print the Python version by running this command:
python3 --version
What does this command do?
The command asks the Python 3 interpreter to print its version. The result tells you whether the Python.org installation is ready.
Windows
- Check specifically for the Python 3.14 runtime by running this command:
py -V:3.14
What does this command do?
The Python launcher selects the installed 3.14 runtime. Its output identifies the exact patch version available on your computer.
✔️ I see version 3.14.7
Python is ready. You now have the exact runtime this project expects.
ⓧ I see an older version
The older interpreter may belong to your operating system or another project. Install the required release without removing the existing interpreter.
- Download the installer from the official Python 3.14.7 release page.
- Run the installer for your operating system.
- Keep the installer's default options.
- Close your current terminal after installation.
- Return to the version check above in a new terminal.
ⓧ Command not found
Your terminal cannot find the required interpreter yet. The official installer adds the Python.org runtime needed by this project.
- Download the installer from the official Python 3.14.7 release page.
- Run the installer for your operating system.
- Keep the installer's default options.
- Close your current terminal after installation.
- Return to the version check above in a new terminal.
Use the Python.org Version on macOS
Apple may provide an older system-controlled Python installation. The Python.org installer adds Python 3.14.7 separately while leaving Apple's copy untouched.
Visual Studio Code provides the editor where you will create files and run the bot. Its Python extension connects the editor to your interpreter.
macOS
- Download Visual Studio Code from the official macOS setup page.
- Open the downloaded installer.
- Move Visual Studio Code into your Applications folder.
Windows
- Download the recommended User Setup installer from the official Windows setup page.
- Run the downloaded installer.
- Keep the installer's default options.
- Press Cmd+Space on macOS or the Windows key on Windows.
- Type Visual Studio Code into the search bar.
- Press Enter to open Visual Studio Code.
- Select the Extensions icon in the left sidebar.
- Enter Python in the extension search field.
- Select the Python extension.
- Click Install on its extension page.
The extension page now identifies the Python extension as installed. Your editor can discover the interpreter you verified above.
Python Version Still Missing?
Close every open terminal after installing Python. A new terminal reloads the updated command path.
On macOS, confirm that you used the Python.org installer instead of Apple's system-controlled interpreter.
Ask for help with your exact operating system and output: Help me verify Python 3.14.7 after installing it.
Build the isolated workspace
A virtual environment gives this project its own Python packages. The pinned versions in requirements.txt make that environment reproducible.
- Click File in the Visual Studio Code menu bar.
- Select Open Folder.
- Select your Desktop in the folder dialog.
- Click New Folder.
- Name the new folder algo-trading-bot.
- Select algo-trading-bot as the folder to open.
Visual Studio Code now shows the empty algo-trading-bot folder in its Explorer sidebar.
- Create requirements.txt from the Explorer sidebar's New File button.
- Paste these pinned dependencies into requirements.txt:
alpaca-py==0.44.0
pandas==3.0.5
python-dotenv==1.2.3
matplotlib==3.11.1
What Do These Dependencies Provide?
- alpaca-py connects the project to Alpaca market data and paper trading.
- pandas handles candle data and RSI calculations.
- python-dotenv loads private settings from .env.
- Matplotlib draws the dashboard charts.
- Save requirements.txt.
The Explorer now lists requirements.txt. The editor shows four dependency pins.
Dependency File Looks Different?
Check that the file is named exactly requirements.txt. Confirm that each dependency occupies its own line.
Ask for a line-by-line check: Help me compare my requirements file with the required package pins.
- Select Terminal in the Visual Studio Code menu bar.
- Select New Terminal.
The dependency download can pause while packages are retrieved. Progress lines in the terminal mean the installation is still working.
macOS
- Create .venv with Python 3. Activate it before installing the dependencies by running:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
What Do These Commands Do?
- The first command creates the isolated .venv environment.
- The second command activates that environment in the current terminal.
- The third command installs every pinned dependency from requirements.txt.
Your terminal prompt now starts with (.venv). The installation finishes with package installation output and no unresolved error.
Installation Failed on macOS?
Confirm that the terminal is inside the algo-trading-bot folder. Confirm that requirements.txt appears in the Explorer.
Ask for help with the complete output: Help me troubleshoot my macOS virtual environment installation.
Windows
- Create .venv with Python. Activate it before installing the dependencies by running:
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
What Do These Commands Do?
- The first command creates the isolated .venv environment.
- The second command activates that environment in PowerShell.
- The third command installs every pinned dependency from requirements.txt.
Your PowerShell prompt now starts with (.venv). The installation finishes with package installation output and no unresolved error.
Installation Failed on Windows?
Confirm that PowerShell is inside the algo-trading-bot folder. Confirm that requirements.txt appears in the Explorer.
If PowerShell blocks activation scripts, use the Windows guidance in the official virtual environment documentation.
Ask for help with the complete output: Help me troubleshoot my Windows virtual environment installation.
Before you test the GUI toolkit, what kind of window do you expect Python to open?
- Verify Tkinter from the activated virtual environment by running:
python -m tkinter
What Should You See?
Python opens a small Tk demonstration window. That window proves the GUI toolkit required by the dashboard works on your computer.
- Close the Tk demonstration window.
That is the first environment checkpoint complete. Your Python installation can open the same kind of desktop window the trading dashboard will use.
Tk Window Did Not Open?
Confirm that the terminal prompt starts with (.venv). Rerun the check after activating the environment if that prefix is missing.
If Python reports a missing GUI dependency, reinstall Python 3.14.7 using the official installer for your operating system.
Ask for help with the exact result: Help me diagnose why the Tkinter demonstration window does not open.
Protect your paper credentials
Environment variables keep account-specific values outside the Python source files. The public template documents each setting while .gitignore keeps the private copy away from version control.
- Create .env.example from the Explorer sidebar's New File button.
- Paste the complete environment template into .env.example:
ALPACA_API_KEY=your-paper-key-here
ALPACA_SECRET_KEY=your-paper-secret-here
SYMBOL=BTC/USD
SIGNAL_MINUTES=15
CHART_MINUTES=1
RSI_PERIOD=14
RSI_ENTRY=50
RSI_EXIT=50
TARGET_QTY=1
FALLBACK_EQUITY_PCT=0.75
POLL_SECONDS=60
What Does This Template Store?
The first two entries are placeholders for separate Alpaca paper credentials. The remaining entries define the market symbol and strategy settings used later.
- Save .env.example.
The Explorer now lists .env.example. Its first two lines contain placeholders instead of real credentials.
Environment Template Missing a Setting?
Compare the file against the complete reference package near the end of this step. Keep every setting on its own line.
Ask for a safe comparison without sharing credentials: Help me verify my environment template.
- Create .gitignore from the Explorer sidebar's New File button.
- Paste these exclusions into .gitignore:
.env
.venv/
__pycache__/
*.pyc
trades.csv
bot.log
What Does This File Protect?
The exclusions cover the private environment file and local virtual environment. They also cover generated Python caches and future bot logs.
- Save .gitignore.
The Explorer now lists .gitignore. Its first line excludes the private .env file.
Hidden Files Not Visible?
Use the Visual Studio Code Explorer to confirm the files exist. Their names begin with a dot because they are configuration files.
Ask for help locating them: Help me find my dotfiles in Visual Studio Code.
- Create README.md from the Explorer sidebar's New File button.
- Paste the complete project instructions into README.md:
# Algo Trading Bot
A long-only BTC/USD RSI crossover bot for Alpaca paper trading.
## Windows PowerShell
```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
python run.py
```
## macOS Terminal
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python run.py
```
Add only Alpaca paper credentials to `.env`. The code uses `paper=True` and is not configured for live trading.
What Does the README Record?
The README preserves the setup commands for both operating systems. Its final line documents the project's paper-only boundary.
- Save README.md.
The Markdown preview now shows separate Windows PowerShell and macOS Terminal sections.
README Formatting Looks Broken?
Compare every code fence against the complete reference package below. A missing backtick can merge the two operating-system sections.
Ask for a formatting check: Help me check the Markdown fences in my README.
- Select algo-trading-bot in the Explorer sidebar.
- Click the New Folder button at the top of the Explorer sidebar.
- Enter algo_bot as the folder name.
- Press Enter.
You should now see the empty algo_bot folder inside algo-trading-bot.
- Select algo_bot in the Explorer sidebar.
- Click the New File button at the top of the Explorer sidebar.
- Enter __init__.py as the file name.
- Press Enter.
- Add the package description by pasting the code below:
"""Algo Trading Bot package."""
What Does This File Do?
The package marker gives the bot's Python modules a shared home. Its docstring states the package purpose.
- Save algo_bot/__init__.py.
The Explorer now shows __init__.py nested inside algo_bot.
Package File in the Wrong Place?
Confirm that algo_bot is a folder. Confirm that __init__.py appears inside it.
Ask for help checking the file tree: Help me verify my Python package structure.
API credentials deserve extra care because they authorize account access. These keys belong only in the private .env file that stays on your computer.
- Open the official Alpaca paper trading guide.
- Create an Alpaca account.
- Sign in to the Alpaca dashboard.
- Switch to the Paper Trading account.
- Generate paper API keys.
- Store both paper credentials in a secure password manager.
Alpaca paper credentials are separate from live-account credentials. Paper trading uses simulated fills without placing live-money orders.
- Copy the environment template into a private .env file using the command for your operating system below:
macOS
cp .env.example .env
What Does This Command Do?
The command copies the safe template into a local .env file. You can now replace its credential placeholders without changing the public template.
Windows
Copy-Item .env.example .env
What Does This Command Do?
The command copies the safe template into a local .env file. You can now replace its credential placeholders without changing the public template.
- Select .env in the Explorer sidebar.
- Replace your-paper-key-here with your Alpaca paper API key.
- Replace your-paper-secret-here with your Alpaca paper secret key.
- Save .env.
- Close the .env editor tab.
- Confirm that .gitignore still lists .env on its first line.
Your private settings now contain paper credentials. The public .env.example still contains safe placeholders.
Paper Credentials Not Available?
Confirm that the dashboard is using the Paper Trading account. Paper credentials come from that account instead of the live account.
Never paste either credential into a help request. Ask for navigation help without sharing the values: Help me locate my Alpaca paper API credentials safely.
Copy/Paste Files.
Use this package to compare every public project file. The private .env file is intentionally excluded because it contains your credentials.
✔️ Awesome, I've got everything!
Your public setup files are complete. Keep .env private before moving on.
ⓧ I'd like to double check the full code
alpaca-py==0.44.0
pandas==3.0.5
python-dotenv==1.2.3
matplotlib==3.11.1
ALPACA_API_KEY=your-paper-key-here
ALPACA_SECRET_KEY=your-paper-secret-here
SYMBOL=BTC/USD
SIGNAL_MINUTES=15
CHART_MINUTES=1
RSI_PERIOD=14
RSI_ENTRY=50
RSI_EXIT=50
TARGET_QTY=1
FALLBACK_EQUITY_PCT=0.75
POLL_SECONDS=60
.env
.venv/
__pycache__/
*.pyc
trades.csv
bot.log
# Algo Trading Bot
A long-only BTC/USD RSI crossover bot for Alpaca paper trading.
## Windows PowerShell
```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
python run.py
```
## macOS Terminal
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python run.py
```
Add only Alpaca paper credentials to `.env`. The code uses `paper=True` and is not configured for live trading.
"""Algo Trading Bot package."""
Before the final check, do you expect the same Tk demonstration window to open from your completed environment?
- Return to the activated terminal in Visual Studio Code.
- Run the final GUI toolkit check:
python -m tkinter
What Does This Final Check Prove?
You should see the small Tk demonstration window again. This confirms that the activated .venv can launch desktop interfaces after the dependencies are installed.
- Keep the Tk demonstration window open for the checkpoint.
- Keep the private .env editor tab closed.
Your workspace is ready for safe paper-trading development. Next, you will expose why a threshold-only trading rule repeats decisions.
Catch the Repeated-Signal Bug
Your local workspace is ready for its first trading decision. This test stays fully offline, so your Alpaca Paper Trading account remains untouched.
A threshold-only rule sees one RSI value at a time. Consecutive readings above 50 expose whether it can distinguish the first entry from simply remaining above the line.
In this step, get ready to:
- Create a deterministic demo with fixed RSI readings.
- Run the threshold-only rule against two readings above 50.
- Compare its decisions with the intended entry followed by HOLD.
Create the deterministic demo
Fixed input turns market timing into a repeatable experiment. The values 49.0, 51.0, and 52.0 cross above the entry level once before staying above it.
- Click the New File icon in the Explorer sidebar.
- Enter threshold_demo.py as the new file name.
You will see a blank threshold_demo.py editor. The file also appears directly inside algo-trading-bot in the Explorer sidebar.
- Populate threshold_demo.py by pasting this complete file:
"""Demonstrate why a threshold-only trading rule repeats actions."""
SAMPLE_RSI = [49.0, 51.0, 52.0]
def naive_action(value: float, entry_level: float = 50.0) -> str:
return "BUY" if value > entry_level else "HOLD"
if __name__ == "__main__":
actions = [naive_action(value) for value in SAMPLE_RSI[1:]]
print("RSI values:", SAMPLE_RSI)
print("Naive actions:", ", ".join(actions))
print("Problem: the second BUY is not a new crossover.")
What does this code do?
- The SAMPLE_RSI list stores three fixed readings so every run follows the same path.
- The naive_action() function checks one reading against the entry level.
- A reading above 50.0 returns BUY.
- The main block evaluates the final two readings before printing both decisions.
- Save threshold_demo.py.
- Confirm that the Explorer sidebar lists threshold_demo.py beside requirements.txt.
Can't find the new file?
Check that threshold_demo.py sits directly inside algo-trading-bot. If it appears inside algo_bot, move it up one level in the Explorer sidebar.
Ask for help with the file location: Help me place threshold_demo.py directly inside my algo-trading-bot folder in VS Code.
Run the threshold rule
The function only knows the current RSI value. Running it reveals how that limited view handles 51.0 followed by 52.0.
Before the run, what do you expect the rule to do with two consecutive values above 50.
- Return to the activated terminal from earlier.
- Execute the deterministic demo by running this command:
python threshold_demo.py
What should I see?
- The first line prints RSI values: [49.0, 51.0, 52.0].
- The decision line prints Naive actions: BUY, BUY.
- The final line prints Problem: the second BUY is not a new crossover..
You caught the bug in a repeatable test. The second BUY happens because the rule sees another value above 50.
The intended decisions are BUY for the first move above 50. The next reading should produce HOLD because no new crossover occurred.
Demo not running?
If the terminal cannot find the file, confirm that it is using the algo-trading-bot folder. If the decisions differ, compare your file with the complete reference below.
Get help diagnosing the demo: Help me troubleshoot threshold_demo.py when python threshold_demo.py does not print Naive actions: BUY, BUY.
Double-check the complete file
The copy/paste package contains the only file created in this step.
✔️ Awesome, I've got everything!
Your saved threshold_demo.py file now reproduces the repeated-signal bug with fixed RSI values.
ⓧ I'd like to double check the full code
"""Demonstrate why a threshold-only trading rule repeats actions."""
SAMPLE_RSI = [49.0, 51.0, 52.0]
def naive_action(value: float, entry_level: float = 50.0) -> str:
return "BUY" if value > entry_level else "HOLD"
if __name__ == "__main__":
actions = [naive_action(value) for value in SAMPLE_RSI[1:]]
print("RSI values:", SAMPLE_RSI)
print("Naive actions:", ", ".join(actions))
print("Problem: the second BUY is not a new crossover.")
What should match?
- Confirm that SAMPLE_RSI contains 49.0, 51.0, and 52.0 in that order.
- Confirm that naive_action() returns BUY whenever the current value exceeds 50.0.
- Confirm that the main block evaluates the final two readings.
The repeated decision is now visible. Next, you will compare consecutive RSI values so the strategy reacts only when the configured level is crossed.
Replace Thresholds with RSI Crossovers
Your threshold demo exposed the bug clearly: two RSI readings above 50 produced two BUY decisions. You now have a repeatable failure that the strategy can fix.
An RSI crossover compares the previous reading with the current reading to detect a boundary crossing. This step turns that comparison into a reusable strategy module powered by pandas.
Position state decides which crossing matters. A flat bot can buy on a cross up. A long bot can sell on a cross down.
In this step, get ready to:
- Trace the one upward crossing in the fixed RSI sequence.
- Create the position-aware crossover strategy module.
- Run the deterministic self-check to confirm one BUY followed by HOLD.
Map the crossing moment
A threshold condition inspects each RSI value alone. Crossover detection compares adjacent readings to find the moment that the sequence moves through the configured level.
- Read the fixed sequence 49.0, 51.0, 52.0 from left to right.
- Mark the move from 49.0 to 51.0 as a cross above 50.0.
- Mark the move from 51.0 to 52.0 as HOLD because both readings remain above the level.
Why does position state matter?
A flat bot can respond to an upward crossing with BUY. A long bot already owns the asset, so another upward reading does not create a second entry.
A long bot can respond to a downward crossing with SELL. Every other combination returns HOLD.
Create and verify the strategy module
The strategy module has three responsibilities. It names each possible signal. It calculates Wilder-style RSI. It chooses an action from the latest two usable RSI readings.
- Select the algo_bot folder in the Explorer sidebar.
- Click New File in the Explorer toolbar.
- Enter strategy.py as the file name.
- Paste the complete file from the first tab into the empty editor.
Copy/Paste Files:
algo_bot/strategy.py
"""RSI calculation and long-only crossover signal logic."""
from __future__ import annotations
from enum import Enum
import pandas as pd
class Signal(str, Enum):
BUY = "BUY"
SELL = "SELL"
HOLD = "HOLD"
def rsi(close: pd.Series, period: int = 14) -> pd.Series:
"""Return Wilder-style RSI aligned to the closing-price series."""
delta = close.diff()
gain = delta.clip(lower=0.0)
loss = -delta.clip(upper=0.0)
avg_gain = gain.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0.0, pd.NA)
return (100 - (100 / (1 + rs))).fillna(100.0).where(avg_gain.notna())
def decide(
rsi_series: pd.Series,
is_long: bool,
entry_level: float,
exit_level: float,
) -> Signal:
"""Enter on a cross up, exit on a cross down, and never short."""
clean = rsi_series.dropna()
if len(clean) < 2:
return Signal.HOLD
previous, current = float(clean.iloc[-2]), float(clean.iloc[-1])
if not is_long and previous <= entry_level < current:
return Signal.BUY
if is_long and previous >= exit_level > current:
return Signal.SELL
return Signal.HOLD
if __name__ == "__main__":
sample = pd.Series([49.0, 51.0, 52.0])
first = decide(sample.iloc[:2], False, 50.0, 50.0)
second = decide(sample.iloc[1:], False, 50.0, 50.0)
print(f"Crossover actions: {first.value}, {second.value}")
Placement Check
The complete file path is algo_bot/strategy.py. The existing threshold_demo.py file stays unchanged as the comparison case.
How does the strategy work?
- The Signal enum gives every decision a fixed name.
- The rsi() function converts closing prices into Wilder-style RSI.
- The decide() function removes missing startup values before comparing the final two usable readings.
- The module self-check sends the same fixed values through two adjacent comparison windows.
- Save algo_bot/strategy.py.
- Switch back to the terminal panel from earlier.
- Run the module self-check with the command below:
Before you run it, predict whether the third RSI reading creates another BUY or returns HOLD.
python -m algo_bot.strategy
You will see Crossover actions: BUY, HOLD. The upward crossing triggers once, while the next reading stays above the level without repeating the entry.
Not seeing BUY, HOLD?
Confirm that strategy.py is inside the algo_bot folder. Confirm that the terminal still has .venv activated.
Compare your file with the complete algo_bot/strategy.py tab above. Pay close attention to the slices passed into decide().
Help me troubleshoot the crossover self-check.
That is the repeated-signal bug fixed. Your strategy now reacts to the crossing event instead of every reading on the same side of the threshold.
Map the three decision paths
- A flat position returns BUY when the previous RSI is at or below the entry level and the current RSI is above it.
- A long position returns SELL when the previous RSI is at or above the exit level and the current RSI is below it.
- Every other state returns HOLD.
Your strategy now reacts once to a crossing. Next, you will feed it completed BTC/USD candles from Alpaca and compare the resulting signal with the current paper position.
Connect Live BTC/USD Candles
Your RSI crossover strategy now separates a single crossing from repeated readings. The next test is whether that same logic can interpret live BTC/USD data.
Fixed samples prove that the crossover rule works. They cannot show what the bot would decide from the market now.
Alpaca supplies crypto candles through its market data API. Your paper account supplies the position state needed for a long-only decision.
This step connects those inputs in a read-only preview. No orders are submitted.
In this step, get ready to:
- Load private paper credentials and strategy settings from .env.
- Add a broker wrapper for crypto data and paper-account state.
- Run a read-only preview that excludes the forming candle.
Load private configuration
Environment variables keep credentials outside your Python files. The configuration module reads your private .env file while preserving the shared defaults from .env.example.
- In the VS Code Explorer sidebar, select .gitignore from earlier.
- Confirm that .env is listed on the first line.
- Create config.py inside the algo_bot folder by using the complete file below:
✔️ Awesome, I've got everything!
Your configuration module matches the complete project file.
ⓧ I'd like to double check the full code
- Replace the full contents of algo_bot/config.py with this file:
"""Configuration loaded from the private .env file."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(PROJECT_ROOT / ".env")
TRADE_LOG_PATH = PROJECT_ROOT / "trades.csv"
@dataclass(frozen=True)
class Config:
api_key: str
secret_key: str
symbol: str
signal_minutes: int
chart_minutes: int
rsi_period: int
rsi_entry: float
rsi_exit: float
target_qty: float
fallback_equity_pct: float
poll_seconds: int
def _require(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise RuntimeError(
f"Missing {name} in .env. Copy .env.example to .env and add paper keys."
)
return value
def load_config() -> Config:
return Config(
api_key=_require("ALPACA_API_KEY"),
secret_key=_require("ALPACA_SECRET_KEY"),
symbol=os.getenv("SYMBOL", "BTC/USD"),
signal_minutes=int(os.getenv("SIGNAL_MINUTES", "15")),
chart_minutes=int(os.getenv("CHART_MINUTES", "1")),
rsi_period=int(os.getenv("RSI_PERIOD", "14")),
rsi_entry=float(os.getenv("RSI_ENTRY", "50")),
rsi_exit=float(os.getenv("RSI_EXIT", "50")),
target_qty=float(os.getenv("TARGET_QTY", "1")),
fallback_equity_pct=float(os.getenv("FALLBACK_EQUITY_PCT", "0.75")),
poll_seconds=int(os.getenv("POLL_SECONDS", "60")),
)
What does this file do?
- PROJECT_ROOT locates the main algo-trading-bot folder from inside the package.
- load_dotenv loads values from the private .env file.
- Config stores credentials and strategy settings in one immutable object.
- _require stops the app when either paper credential is blank.
- Save algo_bot/config.py.
- Confirm that config.py appears inside the algo_bot folder in the Explorer sidebar.
Configuration file in the wrong place?
Check that config.py sits beside strategy.py inside algo_bot. A copy beside run.py belongs to the wrong folder.
Help me check my configuration file location.
Wrap the Alpaca clients
The broker wrapper gives the rest of the app one place to request candles and account state. It also hardcodes paper=True when it creates the trading client.
CryptoHistoricalDataClient handles bars and the latest quote. TradingClient reads the paper position plus account values.
- Create broker.py inside the algo_bot folder by using the complete file below:
✔️ Awesome, I've got everything!
Your broker wrapper matches the complete project file.
ⓧ I'd like to double check the full code
- Replace the full contents of algo_bot/broker.py with this file:
"""Alpaca paper-trading and crypto-data wrapper."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import pandas as pd
from alpaca.data.historical import CryptoHistoricalDataClient
from alpaca.data.requests import CryptoBarsRequest, CryptoLatestQuoteRequest
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import MarketOrderRequest
from .config import Config
SIGNAL_LOOKBACK = timedelta(days=5)
CHART_LOOKBACK = timedelta(hours=8)
@dataclass(frozen=True)
class Position:
qty: float
avg_entry: float
unrealized_pnl: float
unrealized_pnl_pct: float
@staticmethod
def flat() -> "Position":
return Position(0.0, 0.0, 0.0, 0.0)
class Broker:
def __init__(self, config: Config) -> None:
self._config = config
self._data = CryptoHistoricalDataClient(config.api_key, config.secret_key)
self._trading = TradingClient(config.api_key, config.secret_key, paper=True)
def bars(self, minutes: int, lookback: timedelta) -> pd.DataFrame:
request = CryptoBarsRequest(
symbol_or_symbols=[self._config.symbol],
timeframe=TimeFrame(minutes, TimeFrameUnit.Minute),
start=datetime.now(timezone.utc) - lookback,
)
frame = self._data.get_crypto_bars(request).df
if frame.empty:
raise RuntimeError(f"No bars returned for {self._config.symbol}")
return frame.reset_index().set_index("timestamp").sort_index()
def latest_price(self) -> float:
request = CryptoLatestQuoteRequest(
symbol_or_symbols=[self._config.symbol]
)
quote = self._data.get_crypto_latest_quote(request)[self._config.symbol]
bid, ask = float(quote.bid_price), float(quote.ask_price)
if bid > 0 and ask > 0:
return (bid + ask) / 2
return bid or ask
def position(self) -> Position:
key = self._config.symbol.replace("/", "")
for open_position in self._trading.get_all_positions():
if open_position.symbol.replace("/", "") == key:
return Position(
qty=float(open_position.qty),
avg_entry=float(open_position.avg_entry_price),
unrealized_pnl=float(open_position.unrealized_pl or 0.0),
unrealized_pnl_pct=float(open_position.unrealized_plpc or 0.0)
* 100,
)
return Position.flat()
def position_qty(self) -> float:
return self.position().qty
def account_equity(self) -> float:
return float(self._trading.get_account().equity)
def buying_power(self) -> float:
account = self._trading.get_account()
return float(account.non_marginable_buying_power or account.buying_power)
def buy_qty(self, qty: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
qty=round(qty, 8),
side=OrderSide.BUY,
time_in_force=TimeInForce.GTC,
)
)
def buy_notional(self, notional: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
notional=round(notional, 2),
side=OrderSide.BUY,
time_in_force=TimeInForce.GTC,
)
)
def sell(self, qty: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
qty=qty,
side=OrderSide.SELL,
time_in_force=TimeInForce.GTC,
)
)
What does this file do?
- bars requests crypto candles for the configured symbol and interval.
- latest_price uses the midpoint when both bid and ask prices are available.
- position returns the current BTC/USD paper position or a flat position.
- TradingClient always starts with paper=True.
- buy_qty, buy_notional, and sell prepare future order operations. This step does not call them.
- Save algo_bot/broker.py.
- Confirm that broker.py appears beside config.py in the Explorer sidebar.
Broker imports showing warnings?
Confirm that VS Code is using the Python interpreter from your activated .venv. The pinned packages were installed into that environment.
Help me diagnose the broker imports.
Preview a closed-candle decision
A closed candle has a final closing price. The newest candle is still forming, so its RSI value can change before the interval ends.
The preview uses closed = bars.iloc[:-1] to remove that forming candle from the strategy input. This keeps the signal tied to completed market data.
- Create preview.py inside the algo-trading-bot folder by using the complete file below:
✔️ Awesome, I've got everything!
Your read-only preview matches the complete project file.
ⓧ I'd like to double check the full code
- Replace the full contents of preview.py with this file:
"""Read-only preview of the latest closed-candle decision."""
from algo_bot.broker import SIGNAL_LOOKBACK, Broker
from algo_bot.config import load_config
from algo_bot.strategy import decide, rsi
def main() -> None:
config = load_config()
broker = Broker(config)
bars = broker.bars(config.signal_minutes, SIGNAL_LOOKBACK)
closed = bars.iloc[:-1]
values = rsi(closed["close"], config.rsi_period)
position = broker.position()
signal = decide(
values,
position.qty > 0,
config.rsi_entry,
config.rsi_exit,
)
print(f"symbol: {config.symbol}")
print(f"price: ${broker.latest_price():,.2f}")
print(f"closed bar: {closed.index[-1]}")
print(f"rsi({config.rsi_period}): {values.dropna().iloc[-1]:.2f}")
print(f"paper position: {position.qty:.8f}" if position.qty else "paper position: flat")
print(f"signal: {signal.value}")
if __name__ == "__main__":
main()
What does this file do?
- load_config reads the private credentials and strategy settings.
- Broker retrieves BTC/USD bars plus the current paper position.
- bars.iloc[:-1] removes the candle that is still forming.
- rsi calculates the indicator from completed closing prices.
- decide combines the latest RSI crossing with the paper-position state.
- Save preview.py.
- Confirm that preview.py appears beside threshold_demo.py in the Explorer sidebar.
Before you run the preview, predict whether the current market state will produce BUY, SELL, or HOLD.
- Run the read-only market preview from the activated terminal by using this command:
python preview.py
What should I see?
You should see six lines describing the latest read-only decision. The market values change over time.
- symbol: BTC/USD confirms the configured crypto pair.
- price: shows the latest quoted price.
- closed bar: shows the timestamp of the newest completed 15-minute candle.
- rsi(14): shows the RSI calculated from completed candles.
- paper position: shows either a quantity or flat.
- signal: shows BUY, SELL, or HOLD from the crossover strategy.
That is the live bridge working. Your crossover strategy now evaluates Alpaca candles without submitting an order or creating trades.csv.
Preview not returning market data?
Confirm that your activated terminal still shows the .venv environment. Check that your private .env contains paper-account credentials without extra spaces.
Help me troubleshoot the live preview.
Your strategy now has live candles, a current quote, and paper-account context. Next up, you will turn this snapshot into a responsive dashboard with background polling and paper-only controls.
Copy/Paste Files:
✔️ Awesome, I've got everything!
Your three live-preview files are saved. The terminal output confirms that they work together.
ⓧ I'd like to double check the full code
algo_bot/config.py.
"""Configuration loaded from the private .env file."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(PROJECT_ROOT / ".env")
TRADE_LOG_PATH = PROJECT_ROOT / "trades.csv"
@dataclass(frozen=True)
class Config:
api_key: str
secret_key: str
symbol: str
signal_minutes: int
chart_minutes: int
rsi_period: int
rsi_entry: float
rsi_exit: float
target_qty: float
fallback_equity_pct: float
poll_seconds: int
def _require(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise RuntimeError(
f"Missing {name} in .env. Copy .env.example to .env and add paper keys."
)
return value
def load_config() -> Config:
return Config(
api_key=_require("ALPACA_API_KEY"),
secret_key=_require("ALPACA_SECRET_KEY"),
symbol=os.getenv("SYMBOL", "BTC/USD"),
signal_minutes=int(os.getenv("SIGNAL_MINUTES", "15")),
chart_minutes=int(os.getenv("CHART_MINUTES", "1")),
rsi_period=int(os.getenv("RSI_PERIOD", "14")),
rsi_entry=float(os.getenv("RSI_ENTRY", "50")),
rsi_exit=float(os.getenv("RSI_EXIT", "50")),
target_qty=float(os.getenv("TARGET_QTY", "1")),
fallback_equity_pct=float(os.getenv("FALLBACK_EQUITY_PCT", "0.75")),
poll_seconds=int(os.getenv("POLL_SECONDS", "60")),
)
Configuration checkpoint
This file loads the private dotenv values into one immutable configuration object. Its defaults match .env.example.
algo_bot/broker.py.
"""Alpaca paper-trading and crypto-data wrapper."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import pandas as pd
from alpaca.data.historical import CryptoHistoricalDataClient
from alpaca.data.requests import CryptoBarsRequest, CryptoLatestQuoteRequest
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import MarketOrderRequest
from .config import Config
SIGNAL_LOOKBACK = timedelta(days=5)
CHART_LOOKBACK = timedelta(hours=8)
@dataclass(frozen=True)
class Position:
qty: float
avg_entry: float
unrealized_pnl: float
unrealized_pnl_pct: float
@staticmethod
def flat() -> "Position":
return Position(0.0, 0.0, 0.0, 0.0)
class Broker:
def __init__(self, config: Config) -> None:
self._config = config
self._data = CryptoHistoricalDataClient(config.api_key, config.secret_key)
self._trading = TradingClient(config.api_key, config.secret_key, paper=True)
def bars(self, minutes: int, lookback: timedelta) -> pd.DataFrame:
request = CryptoBarsRequest(
symbol_or_symbols=[self._config.symbol],
timeframe=TimeFrame(minutes, TimeFrameUnit.Minute),
start=datetime.now(timezone.utc) - lookback,
)
frame = self._data.get_crypto_bars(request).df
if frame.empty:
raise RuntimeError(f"No bars returned for {self._config.symbol}")
return frame.reset_index().set_index("timestamp").sort_index()
def latest_price(self) -> float:
request = CryptoLatestQuoteRequest(
symbol_or_symbols=[self._config.symbol]
)
quote = self._data.get_crypto_latest_quote(request)[self._config.symbol]
bid, ask = float(quote.bid_price), float(quote.ask_price)
if bid > 0 and ask > 0:
return (bid + ask) / 2
return bid or ask
def position(self) -> Position:
key = self._config.symbol.replace("/", "")
for open_position in self._trading.get_all_positions():
if open_position.symbol.replace("/", "") == key:
return Position(
qty=float(open_position.qty),
avg_entry=float(open_position.avg_entry_price),
unrealized_pnl=float(open_position.unrealized_pl or 0.0),
unrealized_pnl_pct=float(open_position.unrealized_plpc or 0.0)
* 100,
)
return Position.flat()
def position_qty(self) -> float:
return self.position().qty
def account_equity(self) -> float:
return float(self._trading.get_account().equity)
def buying_power(self) -> float:
account = self._trading.get_account()
return float(account.non_marginable_buying_power or account.buying_power)
def buy_qty(self, qty: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
qty=round(qty, 8),
side=OrderSide.BUY,
time_in_force=TimeInForce.GTC,
)
)
def buy_notional(self, notional: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
notional=round(notional, 2),
side=OrderSide.BUY,
time_in_force=TimeInForce.GTC,
)
)
def sell(self, qty: float):
return self._trading.submit_order(
MarketOrderRequest(
symbol=self._config.symbol,
qty=qty,
side=OrderSide.SELL,
time_in_force=TimeInForce.GTC,
)
)
Broker checkpoint
This file reads crypto market data and paper-account state. The trading client remains locked to paper=True.
preview.py.
"""Read-only preview of the latest closed-candle decision."""
from algo_bot.broker import SIGNAL_LOOKBACK, Broker
from algo_bot.config import load_config
from algo_bot.strategy import decide, rsi
def main() -> None:
config = load_config()
broker = Broker(config)
bars = broker.bars(config.signal_minutes, SIGNAL_LOOKBACK)
closed = bars.iloc[:-1]
values = rsi(closed["close"], config.rsi_period)
position = broker.position()
signal = decide(
values,
position.qty > 0,
config.rsi_entry,
config.rsi_exit,
)
print(f"symbol: {config.symbol}")
print(f"price: ${broker.latest_price():,.2f}")
print(f"closed bar: {closed.index[-1]}")
print(f"rsi({config.rsi_period}): {values.dropna().iloc[-1]:.2f}")
print(f"paper position: {position.qty:.8f}" if position.qty else "paper position: flat")
print(f"signal: {signal.value}")
if __name__ == "__main__":
main()
Preview checkpoint
This file excludes the forming candle before calculating RSI. It prints the resulting decision without calling any order method.
Launch the Paper-Trading Dashboard
Your read-only preview has proved that Alpaca can return BTC/USD market data plus paper-account state. The next goal is a dashboard that keeps this information visible while the strategy runs.
A terminal snapshot cannot continuously expose the bot's state or safely coordinate network polling with desktop controls. Tkinter provides the desktop event loop. Matplotlib renders the price and RSI charts inside that interface.
In this step, get ready to:
- Build a background engine for closed-candle polling.
- Connect a responsive dashboard to queued state updates.
- Verify the dashboard with one manual simulated paper order.
Build the background trading engine
The engine keeps network work away from the desktop event loop. It also remembers the last traded candle so one crossover cannot trigger duplicate orders during repeated polling.
The Copy/Paste Files below split the engine into market polling, paper execution, and lifecycle coordination. Together, the modules preserve closed-candle decisions plus CSV activity records.
- Select the existing algo_bot folder in the VS Code Explorer sidebar.
- Create engine_market.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete market module from its tab into algo_bot/engine_market.py.
- Create engine_execution.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete execution module from its tab into algo_bot/engine_execution.py.
- Create engine.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete coordinator module from its tab into algo_bot/engine.py.
algo_bot/engine_market.py
"""State model and market-data polling for the trading engine."""
from __future__ import annotations
from dataclasses import dataclass, field
import pandas as pd
from .broker import CHART_LOOKBACK, SIGNAL_LOOKBACK
from .strategy import Signal, decide, rsi
PRICE_TICK_SECONDS = 3
@dataclass
class BotState:
"""Hold the latest values required by the dashboard."""
running: bool = False
connected: bool = False
price: float = 0.0
rsi: float = 0.0
equity: float = 0.0
position_qty: float = 0.0
avg_entry: float = 0.0
unrealized_pnl: float = 0.0
unrealized_pnl_pct: float = 0.0
last_signal: str = Signal.HOLD.value
last_bar_time: str = "--"
error: str = ""
bars: pd.DataFrame | None = None
rsi_series: pd.Series | None = None
rsi_live: float = 0.0
forming_time: pd.Timestamp | None = None
events: list[str] = field(default_factory=list)
class MarketMixin:
"""Fetch market data and publish complete state snapshots."""
def _tick_loop(self) -> None:
# Refresh fast-moving quote values between slower strategy polls.
while not self._stop_ticker.is_set():
try:
self._tick()
except Exception as error:
print(f"price tick failed: {error}")
self._stop_ticker.wait(PRICE_TICK_SECONDS)
def _tick(self) -> None:
# Revalue the current paper position using the latest quote.
position = self._broker.position()
price = self._broker.latest_price()
pnl = (price - position.avg_entry) * position.qty if position.qty else 0.0
cost = position.avg_entry * position.qty
live: dict[str, object] = {}
if self._signal_close is not None:
rsi_live, forming_time = self._provisional(self._signal_close, price)
live = {"rsi_live": rsi_live, "forming_time": forming_time}
self.state = BotState(
**{
**self.state.__dict__,
**live,
"connected": True,
"running": self.is_running,
"events": list(self._events),
"price": price,
"position_qty": position.qty,
"avg_entry": position.avg_entry,
"unrealized_pnl": pnl,
"unrealized_pnl_pct": (pnl / cost * 100) if cost else 0.0,
}
)
self._on_update(self.state)
def _poll(self, trade: bool = True) -> None:
# Use closed signal candles while keeping shorter bars for the chart.
bars = self._broker.bars(self._config.signal_minutes, SIGNAL_LOOKBACK)
closed = bars.iloc[:-1]
chart_bars = self._broker.bars(
self._config.chart_minutes, CHART_LOOKBACK
)
rsi_series = rsi(closed["close"], self._config.rsi_period)
position = self._broker.position()
qty = position.qty
price = float(bars["close"].iloc[-1])
bar_time = closed.index[-1]
# Calculate a provisional RSI point for display only.
self._signal_close = closed["close"]
rsi_live, forming_time = self._provisional(closed["close"], price)
signal = decide(
rsi_series,
qty > 0,
self._config.rsi_entry,
self._config.rsi_exit,
)
# Execute at most once for each completed signal candle.
if trade and signal is not Signal.HOLD and bar_time != self._last_traded_bar:
self._last_traded_bar = bar_time
qty = self._execute(
signal,
qty,
price,
float(rsi_series.dropna().iloc[-1]),
)
self.state = BotState(
running=self.is_running,
connected=True,
price=price,
rsi=float(rsi_series.dropna().iloc[-1]),
equity=self._broker.account_equity(),
position_qty=qty,
avg_entry=position.avg_entry,
unrealized_pnl=position.unrealized_pnl,
unrealized_pnl_pct=position.unrealized_pnl_pct,
last_signal=signal.value,
last_bar_time=bar_time.strftime("%Y-%m-%d %H:%M UTC"),
error="",
bars=chart_bars,
rsi_series=rsi_series,
rsi_live=rsi_live,
forming_time=forming_time,
events=list(self._events),
)
self._on_update(self.state)
def _provisional(
self, closed_close: pd.Series, price: float
) -> tuple[float, pd.Timestamp]:
# Append the live quote at the next interval without trading on it.
forming_time = closed_close.index[-1] + self._bar_length
series = pd.concat(
[closed_close, pd.Series([price], index=[forming_time])]
)
value = float(rsi(series, self._config.rsi_period).dropna().iloc[-1])
return value, forming_time
algo_bot/engine_execution.py
"""Paper-order execution and CSV activity logging."""
from __future__ import annotations
import csv
from datetime import datetime, timezone
from typing import Any
from .config import TRADE_LOG_PATH
from .strategy import Signal
MAX_EVENTS = 200
COST_BUFFER = 1.002
class ExecutionMixin:
"""Submit paper orders and record their estimated details."""
_broker: Any
_config: Any
_events: list[str]
def _execute(
self, signal: Signal, qty: float, price: float, rsi_value: float
) -> float:
# Route BUY and SELL signals to their paper-order paths.
if signal is Signal.BUY:
return self._buy(price, rsi_value, qty)
self._broker.sell(qty)
self._log(f"SELL {qty:.6f} @ ~${price:,.2f} (RSI {rsi_value:.1f})")
self._record_trade("SELL", qty, price, rsi_value)
return 0.0
def _buy(self, price: float, rsi_value: float, current_qty: float) -> float:
# Prefer the configured quantity when paper buying power covers it.
target = self._config.target_qty
buying_power = self._broker.buying_power()
if buying_power >= target * price * COST_BUFFER:
self._broker.buy_qty(target)
self._log(
f"BUY {target:g} {self._config.symbol.split('/')[0]} "
f"@ ~${price:,.2f} (RSI {rsi_value:.1f})"
)
self._record_trade("BUY", target, price, rsi_value)
return target
# Fall back to a bounded share of paper-account equity.
notional = min(
self._broker.account_equity() * self._config.fallback_equity_pct,
buying_power / COST_BUFFER,
)
if notional <= 0:
self._log("BUY skipped: no paper buying power")
return current_qty
self._broker.buy_notional(notional)
percent = self._config.fallback_equity_pct * 100
self._log(
f"BUY ${notional:,.2f} ({percent:g}% of equity fallback) "
f"@ ~${price:,.2f} (RSI {rsi_value:.1f})"
)
self._record_trade("BUY", notional / price, price, rsi_value)
return notional / price
def _record_trade(
self, side: str, qty: float, price: float, rsi_value: float
) -> None:
# Create the header once before appending the paper execution.
is_new = not TRADE_LOG_PATH.exists()
with TRADE_LOG_PATH.open("a", newline="", encoding="utf-8") as handle:
writer = csv.writer(handle)
if is_new:
writer.writerow(
["timestamp_utc", "symbol", "side", "qty", "price", "rsi"]
)
writer.writerow(
[
datetime.now(timezone.utc).isoformat(timespec="seconds"),
self._config.symbol,
side,
f"{qty:.8f}",
f"{price:.2f}",
f"{rsi_value:.2f}",
]
)
def _log(self, message: str) -> None:
# Keep a bounded in-memory activity feed for the dashboard.
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
self._events.append(f"[{stamp}Z] {message}")
del self._events[:-MAX_EVENTS]
algo_bot/engine.py
"""Coordinate background polling for the paper-trading dashboard."""
from __future__ import annotations
import threading
import traceback
from typing import Callable
import pandas as pd
from .broker import SIGNAL_LOOKBACK, Broker
from .config import Config
from .engine_execution import ExecutionMixin
from .engine_market import BotState, MarketMixin
from .strategy import Signal, rsi
class TradingEngine(MarketMixin, ExecutionMixin):
"""Coordinate lifecycle, market polling, and paper execution."""
def __init__(self, config: Config, on_update: Callable[[BotState], None]) -> None:
# Store shared collaborators used by both mixins.
self._config = config
self._on_update = on_update
self._broker = Broker(config)
self._stop = threading.Event()
self._stop_ticker = threading.Event()
self._thread: threading.Thread | None = None
self._ticker: threading.Thread | None = None
self._events: list[str] = []
self._last_traded_bar: pd.Timestamp | None = None
self._bar_length = pd.Timedelta(minutes=config.signal_minutes)
self._signal_close: pd.Series | None = None
self.state = BotState()
@property
def is_running(self) -> bool:
# The bot runs only while its worker thread remains active.
return bool(
self._thread
and self._thread.is_alive()
and not self._stop.is_set()
)
def start(self) -> None:
# Start one strategy worker and ignore duplicate clicks.
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def start_price_feed(self) -> None:
# Start one lightweight quote ticker for live dashboard values.
if self._ticker and self._ticker.is_alive():
return
self._stop_ticker.clear()
self._ticker = threading.Thread(target=self._tick_loop, daemon=True)
self._ticker.start()
def run_once(self) -> BotState:
# Expose one synchronous cycle for checks and previews.
self._poll(trade=True)
return self.state
def stop(self) -> None:
# Stop strategy polling while leaving the quote display available.
self._stop.set()
self._log("Bot stopped by user")
self.state = BotState(**{**self.state.__dict__, "running": False})
self._on_update(self.state)
def shutdown(self) -> None:
# Stop both background loops before the window closes.
self.stop()
self._stop_ticker.set()
def blitz(self) -> str:
# Submit one manual paper order using the current position direction.
bars = self._broker.bars(self._config.signal_minutes, SIGNAL_LOOKBACK)
closed = bars.iloc[:-1]
qty = self._broker.position_qty()
price = float(bars["close"].iloc[-1])
rsi_now = float(
rsi(closed["close"], self._config.rsi_period).dropna().iloc[-1]
)
side = Signal.SELL if qty > 0 else Signal.BUY
self._log(f"BLITZ manual {side.value}")
self._execute(side, qty, price, rsi_now)
self._poll(trade=self.is_running)
return side.value
def log_error(self, message: str) -> None:
# Publish worker failures through the same dashboard state channel.
self._log(f"ERROR: {message}")
self.state = BotState(
**{**self.state.__dict__, "error": message, "events": list(self._events)}
)
self._on_update(self.state)
def _run(self) -> None:
# Poll until the user stops the bot, then wait between cycles.
self._log(
f"Bot started on {self._config.symbol} "
f"({self._config.signal_minutes}m RSI-{self._config.rsi_period}, long only)"
)
while not self._stop.is_set():
try:
self._poll()
except Exception as error:
self._log(f"ERROR: {error}")
traceback.print_exc()
self.state = BotState(
**{
**self.state.__dict__,
"connected": False,
"error": str(error),
"events": list(self._events),
}
)
self._on_update(self.state)
self._stop.wait(self._config.poll_seconds)
What do these engine modules do?
- engine_market.py stores BotState and calculates closed-candle strategy updates.
- engine_execution.py submits simulated paper orders and appends them to trades.csv.
- engine.py starts the background workers and keeps one TradingEngine interface for the dashboard.
- _last_traded_bar prevents the same completed candle from triggering another order.
- Save algo_bot/engine_market.py.
- Save algo_bot/engine_execution.py.
- Save algo_bot/engine.py.
- Confirm that all three engine files appear inside algo_bot in the Explorer sidebar.
Seeing engine import warnings?
- Switch VS Code back to the .venv interpreter you selected earlier.
- Confirm that engine_market.py, engine_execution.py, and engine.py sit together inside algo_bot.
- Confirm that engine.py imports MarketMixin and ExecutionMixin from the two helper modules.
Help me diagnose the split engine imports.
Create the dashboard interface
A worker thread must never change a Tkinter widget directly. The dashboard sends each BotState through a queue. Tkinter's after method drains that queue from the event-loop thread.
The next Copy/Paste Files separate widget construction, chart rendering, and dashboard coordination. The public Dashboard class remains in gui.py.
- Select the existing algo_bot folder in the VS Code Explorer sidebar.
- Click the New File icon in the Explorer sidebar.
- Enter theme.py as the file name.
- Paste this complete Copy/Paste File into algo_bot/theme.py:
"""Cross-platform dark dashboard palette."""
from __future__ import annotations
import platform
BG = "#0d1117"
PANEL = "#151b23"
BORDER = "#232c38"
TEXT = "#e6edf3"
MUTED = "#7d8894"
GREEN = "#3fb950"
RED = "#f85149"
AMBER = "#d29922"
ACCENT = "#58a6ff"
PURPLE = "#bc8cff"
AXIS = "#f2cc60"
RSI_UPPER = RED
RSI_LOWER = GREEN
if platform.system() == "Darwin":
SANS = "Helvetica Neue"
MONO = "Menlo"
else:
SANS = "Segoe UI"
MONO = "Consolas"
FONT = (SANS, 10)
FONT_SMALL = (SANS, 9)
FONT_LABEL = (SANS, 8)
FONT_STAT = (MONO, 20, "bold")
FONT_TITLE = (SANS, 15, "bold")
FONT_MONO = (MONO, 9)
MPL_STYLE = {
"figure.facecolor": BG,
"axes.facecolor": PANEL,
"axes.edgecolor": BORDER,
"axes.labelcolor": AXIS,
"text.color": TEXT,
"xtick.color": AXIS,
"ytick.color": AXIS,
"grid.color": BORDER,
"font.size": 8,
}
What does the theme control?
- The color constants keep Tkinter widgets plus Matplotlib charts on one dark palette.
- The platform check selects familiar fonts for macOS or Windows.
- MPL_STYLE gives both charts consistent axes and grid colors.
- Save algo_bot/theme.py.
- Confirm theme.py is listed beside engine.py in the Explorer sidebar.
Seeing a theme syntax warning?
- Confirm every color value keeps its opening and closing quotation marks.
- Confirm the closing brace for MPL_STYLE remains at the end of the file.
Help me compare my theme file with the complete version.
- Select the existing algo_bot folder in the VS Code Explorer sidebar.
- Create gui_layout.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete layout module from its tab into algo_bot/gui_layout.py.
- Create gui_charts.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete chart module from its tab into algo_bot/gui_charts.py.
- Create gui.py inside algo_bot by using the Explorer sidebar's New File button.
- Paste the complete dashboard module from its tab into algo_bot/gui.py.
algo_bot/gui_layout.py
"""Build the dashboard widgets and chart canvas."""
from __future__ import annotations
import tkinter as tk
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from . import theme
STAT_CARDS = ("price", "rsi", "position", "pnl", "equity", "signal")
class LayoutMixin:
"""Create the header, statistics, charts, and activity panel."""
def _build_header(self) -> None:
# Keep controls and connection state in one compact header.
bar = tk.Frame(self, bg=theme.BG)
bar.pack(fill="x", padx=18, pady=(16, 8))
tk.Label(
bar,
text="ALGO TRADING BOT",
font=theme.FONT_TITLE,
bg=theme.BG,
fg=theme.TEXT,
).pack(side="left")
subtitle = (
f" {self._config.symbol} / {self._config.signal_minutes}m "
f"RSI-{self._config.rsi_period} / long only / PAPER"
)
tk.Label(
bar,
text=subtitle,
font=theme.FONT_SMALL,
bg=theme.BG,
fg=theme.MUTED,
).pack(side="left", padx=(10, 0))
self._stop_button = tk.Button(
bar, text="STOP", width=10, command=self._stop,
bg=theme.PANEL, fg=theme.RED, font=theme.FONT, relief="flat",
activebackground=theme.BORDER, activeforeground=theme.RED,
state="disabled",
)
self._stop_button.pack(side="right", padx=(8, 0))
self._start_button = tk.Button(
bar, text="START BOT", width=12, command=self._start,
bg=theme.GREEN, fg=theme.BG, font=theme.FONT, relief="flat",
activebackground=theme.GREEN,
)
self._start_button.pack(side="right")
self._blitz_button = tk.Button(
bar, text="BLITZ TEST BUY", width=16, command=self._blitz,
bg=theme.PURPLE, fg=theme.BG, font=theme.FONT, relief="flat",
activebackground=theme.PURPLE,
)
self._blitz_button.pack(side="right", padx=(0, 16))
self._status = tk.Label(
bar, text="idle", font=theme.FONT_SMALL,
bg=theme.BG, fg=theme.MUTED,
)
self._status.pack(side="right", padx=(0, 16))
def _build_stats(self) -> None:
# Map state fields to the labels shown on the six statistic cards.
labels = {
"price": "BTC PRICE",
"rsi": f"RSI({self._config.rsi_period}) {self._config.signal_minutes}m",
"position": "POSITION",
"pnl": "UNREALIZED P&L",
"equity": "EQUITY",
"signal": "LAST SIGNAL",
}
row = tk.Frame(self, bg=theme.BG)
row.pack(fill="x", padx=18)
for index, key in enumerate(STAT_CARDS):
row.columnconfigure(index, weight=1)
card = tk.Frame(
row, bg=theme.PANEL,
highlightbackground=theme.BORDER, highlightthickness=1,
)
card.grid(
row=0, column=index, sticky="ew",
padx=(0 if index == 0 else 8, 0),
)
tk.Label(
card, text=labels[key], font=theme.FONT_LABEL,
bg=theme.PANEL, fg=theme.MUTED,
).pack(anchor="w", padx=14, pady=(10, 0))
value = tk.Label(
card, text="--", font=theme.FONT_STAT,
bg=theme.PANEL, fg=theme.TEXT,
)
value.pack(anchor="w", padx=14)
self._stats[key] = value
sub = tk.Label(
card, text=" ", font=theme.FONT_LABEL,
bg=theme.PANEL, fg=theme.MUTED,
)
sub.pack(anchor="w", padx=14, pady=(0, 10))
self._substats[key] = sub
def _build_body(self) -> None:
# Give charts most of the width and reserve a panel for activity.
body = tk.Frame(self, bg=theme.BG)
body.pack(fill="both", expand=True, padx=18, pady=14)
body.columnconfigure(0, weight=3)
body.columnconfigure(1, weight=1)
body.rowconfigure(0, weight=1)
with matplotlib.rc_context(theme.MPL_STYLE):
self._figure = Figure(figsize=(7, 5), dpi=100)
self._price_axes = self._figure.add_subplot(2, 1, 1)
self._rsi_axes = self._figure.add_subplot(
2, 1, 2, sharex=self._price_axes
)
self._figure.subplots_adjust(
left=0.10, right=0.98, top=0.96, bottom=0.10, hspace=0.18
)
self._chart = FigureCanvasTkAgg(self._figure, master=body)
self._chart.get_tk_widget().grid(row=0, column=0, sticky="nsew")
side = tk.Frame(
body, bg=theme.PANEL,
highlightbackground=theme.BORDER, highlightthickness=1,
)
side.grid(row=0, column=1, sticky="nsew", padx=(12, 0))
tk.Label(
side, text="ACTIVITY", font=theme.FONT_LABEL,
bg=theme.PANEL, fg=theme.MUTED,
).pack(anchor="w", padx=12, pady=(10, 6))
self._log = tk.Text(
side, bg=theme.PANEL, fg=theme.TEXT, font=theme.FONT_MONO,
relief="flat", wrap="word", state="disabled",
insertbackground=theme.TEXT,
)
self._log.pack(fill="both", expand=True, padx=8, pady=(0, 10))
self._log.tag_configure("buy", foreground=theme.GREEN)
self._log.tag_configure("sell", foreground=theme.AMBER)
self._log.tag_configure("error", foreground=theme.RED)
self._log.tag_configure("info", foreground=theme.MUTED)
algo_bot/gui_charts.py
"""Render price and RSI charts for the dashboard."""
from __future__ import annotations
import matplotlib
from . import theme
from .engine import BotState
CHART_BARS = 480
RSI_OVERBOUGHT = 70
RSI_OVERSOLD = 30
class ChartMixin:
"""Draw closed-candle history with a provisional live leg."""
@staticmethod
def _draw_live_leg(
axes,
last_time,
last_value,
forming_time,
live_value,
) -> None:
# Show the forming point as a dashed amber extension.
if forming_time is None or not live_value:
return
axes.plot(
[last_time, forming_time],
[last_value, live_value],
color=theme.AMBER,
linewidth=1.1,
linestyle="--",
)
axes.plot(
[forming_time],
[live_value],
marker="o",
markersize=3.5,
color=theme.AMBER,
)
def _render_charts(self, state: BotState) -> None:
# Limit the visible history while aligning RSI with chart bars.
bars = state.bars.tail(CHART_BARS)
rsi_values = state.rsi_series.dropna()
rsi_values = rsi_values[rsi_values.index >= bars.index[0]]
with matplotlib.rc_context(theme.MPL_STYLE):
self._price_axes.clear()
self._price_axes.plot(
bars.index,
bars["close"],
color=theme.ACCENT,
linewidth=1.4,
)
self._price_axes.fill_between(
bars.index,
bars["close"].min(),
bars["close"],
color=theme.ACCENT,
alpha=0.08,
)
self._draw_live_leg(
self._price_axes,
bars.index[-1],
bars["close"].iloc[-1],
state.forming_time,
state.price,
)
self._price_axes.set_ylabel(
f"{self._config.symbol} ({self._config.chart_minutes}m)"
)
self._price_axes.grid(alpha=0.25, linewidth=0.5)
self._price_axes.tick_params(labelbottom=False)
# Draw closed RSI values and configured reference levels.
self._rsi_axes.clear()
self._rsi_axes.plot(
rsi_values.index,
rsi_values,
color=theme.TEXT,
linewidth=1.2,
)
self._rsi_axes.axhline(
RSI_OVERBOUGHT, color=theme.RSI_UPPER, linewidth=0.7
)
self._rsi_axes.axhline(
RSI_OVERSOLD, color=theme.RSI_LOWER, linewidth=0.7
)
for level in {self._config.rsi_entry, self._config.rsi_exit}:
if level not in (RSI_OVERBOUGHT, RSI_OVERSOLD):
self._rsi_axes.axhline(
level,
color=theme.PURPLE,
linestyle=":",
linewidth=0.8,
alpha=0.7,
)
self._draw_live_leg(
self._rsi_axes,
rsi_values.index[-1],
rsi_values.iloc[-1],
state.forming_time,
state.rsi_live,
)
self._rsi_axes.set_yticks([0, RSI_OVERSOLD, 50, RSI_OVERBOUGHT, 100])
self._rsi_axes.set_ylim(0, 100)
self._rsi_axes.set_ylabel(
f"RSI {self._config.rsi_period} ({self._config.signal_minutes}m)"
)
self._rsi_axes.grid(alpha=0.25, linewidth=0.5)
# Reapply palette colors after clearing both axes.
for axes in (self._price_axes, self._rsi_axes):
axes.tick_params(axis="both", colors=theme.AXIS, labelsize=8)
axes.yaxis.label.set_color(theme.AXIS)
for spine in axes.spines.values():
spine.set_edgecolor(theme.BORDER)
self._chart.draw_idle()
algo_bot/gui.py
"""Tkinter dashboard for Algo Trading Bot."""
from __future__ import annotations
import queue
import threading
import tkinter as tk
import matplotlib
matplotlib.use("TkAgg")
from . import theme
from .config import Config
from .engine import BotState, TradingEngine
from .gui_charts import ChartMixin
from .gui_layout import LayoutMixin
REFRESH_MS = 200
class Dashboard(LayoutMixin, ChartMixin, tk.Tk):
"""Display queued trading state without blocking Tkinter."""
def __init__(self, config: Config) -> None:
# Build shared state before creating widgets that reference it.
super().__init__()
self._config = config
self._queue: queue.Queue[BotState] = queue.Queue()
self._engine = TradingEngine(config, self._queue.put)
self.title(
f"Algo Trading Bot - {config.symbol} - "
f"RSI {config.signal_minutes}m Crossover"
)
self.geometry("1180x760")
self.minsize(940, 640)
self.configure(bg=theme.BG)
self._stats: dict[str, tk.Label] = {}
self._substats: dict[str, tk.Label] = {}
self._build_header()
self._build_stats()
self._build_body()
self._render(self._engine.state)
self._engine.start_price_feed()
self.after(REFRESH_MS, self._drain)
self.protocol("WM_DELETE_WINDOW", self._on_close)
def _start(self) -> None:
# Start strategy polling and reflect the active control state.
self._engine.start()
self._start_button.configure(state="disabled")
self._stop_button.configure(state="normal")
self._status.configure(text="connecting", fg=theme.AMBER)
def _stop(self) -> None:
# Stop strategy polling without closing the dashboard.
self._engine.stop()
self._start_button.configure(state="normal")
self._stop_button.configure(state="disabled")
self._status.configure(text="stopped", fg=theme.MUTED)
def _blitz(self) -> None:
# Run the manual paper-order action outside the event loop.
self._blitz_button.configure(state="disabled", text="SENDING...")
threading.Thread(target=self._blitz_worker, daemon=True).start()
def _blitz_worker(self) -> None:
try:
self._engine.blitz()
except Exception as error:
self._engine.log_error(f"BLITZ failed: {error}")
finally:
self.after(
0,
lambda: self._blitz_button.configure(state="normal"),
)
def _on_close(self) -> None:
# Stop background work before destroying the window.
self._engine.shutdown()
self.destroy()
def _drain(self) -> None:
# Render only the newest queued snapshot on each UI cycle.
state = None
while not self._queue.empty():
state = self._queue.get_nowait()
if state is not None:
self._render(state)
self.after(REFRESH_MS, self._drain)
def _render(self, state: BotState) -> None:
# Update headline values and their state-dependent colors.
self._stats["price"].configure(
text=f"${state.price:,.0f}" if state.price else "--"
)
rsi_color = (
theme.GREEN if state.rsi >= self._config.rsi_entry else theme.RED
)
self._stats["rsi"].configure(
text=f"{state.rsi:.1f}" if state.rsi else "--",
fg=rsi_color if state.rsi else theme.TEXT,
)
self._stats["position"].configure(
text=f"{state.position_qty:.5f}" if state.position_qty else "FLAT",
fg=theme.GREEN if state.position_qty else theme.MUTED,
)
self._substats["position"].configure(
text=f"entry ${state.avg_entry:,.2f}" if state.position_qty else " "
)
self._render_pnl(state)
self._stats["equity"].configure(
text=f"${state.equity:,.0f}" if state.equity else "--"
)
self._stats["signal"].configure(
text=state.last_signal,
fg={"BUY": theme.GREEN, "SELL": theme.AMBER}.get(
state.last_signal, theme.MUTED
),
)
self._substats["signal"].configure(text=self._waiting_for(state))
self._substats["rsi"].configure(
text=(
f"{state.rsi - self._config.rsi_entry:+.1f} vs entry"
if state.rsi
else " "
)
)
# Keep the manual action aligned with the current position.
if self._blitz_button["state"] != "disabled":
self._blitz_button.configure(
text="BLITZ TEST SELL" if state.position_qty else "BLITZ TEST BUY"
)
if state.error:
self._status.configure(text="error", fg=theme.RED)
elif state.connected:
self._status.configure(
text=f"live / bar {state.last_bar_time}", fg=theme.GREEN
)
self._render_log(state.events)
if state.bars is not None and state.rsi_series is not None:
self._render_charts(state)
def _waiting_for(self, state: BotState) -> str:
# Describe the next crossover that can trigger an order.
if not state.running:
return "bot stopped"
if state.position_qty:
return f"exit on cross below {self._config.rsi_exit:g}"
return f"buy on cross above {self._config.rsi_entry:g}"
def _render_pnl(self, state: BotState) -> None:
# Show signed unrealized performance only for an open position.
if not state.position_qty:
self._stats["pnl"].configure(text="--", fg=theme.MUTED)
self._substats["pnl"].configure(text=" ")
return
pnl = state.unrealized_pnl
color = theme.GREEN if pnl > 0 else theme.RED if pnl < 0 else theme.TEXT
sign = "+" if pnl > 0 else "-" if pnl < 0 else ""
self._stats["pnl"].configure(
text=f"{sign}${abs(pnl):,.2f}", fg=color
)
self._substats["pnl"].configure(
text=f"{sign}{abs(state.unrealized_pnl_pct):.2f}%",
fg=color,
)
def _render_log(self, events: list[str]) -> None:
# Rebuild the short activity feed with event-specific colors.
self._log.configure(state="normal")
self._log.delete("1.0", "end")
for event in reversed(events):
if "ERROR" in event:
tag = "error"
elif "BUY" in event:
tag = "buy"
elif "SELL" in event:
tag = "sell"
else:
tag = "info"
self._log.insert("end", event + "\n\n", tag)
self._log.configure(state="disabled")
How do the dashboard modules work together?
- gui_layout.py creates the header, statistic cards, chart canvas, and activity panel.
- gui_charts.py draws the price series, closed-candle RSI, and provisional live legs.
- gui.py drains queue.Queue[BotState] and updates widgets from Tkinter's event-loop thread.
- Dashboard combines both helper mixins while preserving the launcher's existing import.
- Save algo_bot/gui_layout.py.
- Save algo_bot/gui_charts.py.
- Save algo_bot/gui.py.
- Confirm that all three dashboard files appear beside theme.py in the Explorer sidebar.
Seeing dashboard import warnings?
- Confirm that gui_layout.py, gui_charts.py, and gui.py sit together inside algo_bot.
- Confirm that gui.py imports LayoutMixin and ChartMixin from the helper modules.
- Confirm that gui_charts.py ends with self._chart.draw_idle().
Help me diagnose the split dashboard modules.
Launch and test the dashboard
The launcher has one responsibility. It loads your private configuration before handing control to the dashboard's event loop.
- Select the top-level algo-trading-bot folder in the VS Code Explorer sidebar.
- Click the New File icon in the Explorer sidebar.
- Enter run.py as the file name.
- Paste this complete Copy/Paste File into run.py:
"""Launch Algo Trading Bot."""
from algo_bot.config import load_config
from algo_bot.gui import Dashboard
if __name__ == "__main__":
Dashboard(load_config()).mainloop()
What does the launcher do?
load_config() reads the local settings plus paper credentials. Dashboard(load_config()).mainloop() builds the interface before starting its event loop.
- Save run.py.
- Return to the VS Code terminal with the activated .venv.
Before you launch it, what do you expect the window to show before the trading engine starts?
- Launch the dashboard by running:
python run.py
What should you see?
You should see the dark Algo Trading Bot window with six status cards plus two chart areas. The header should identify BTC/USD as a long-only paper strategy.
The latest price feed begins independently. The strategy remains stopped until you use the start control.
- Click START BOT in the dashboard header.
Give the first network refresh a moment to complete. You should see the status change to live plus the price and RSI charts fill with data.
- Confirm the position card shows either FLAT or your current simulated paper quantity.
- Confirm the activity panel records that the bot started on BTC/USD.
Dashboard stuck while connecting?
- Confirm the computer still has an internet connection.
- Confirm the private .env contains your paper-account credentials.
- Confirm you remain signed into the Alpaca Paper Trading account that issued those credentials.
Help me diagnose why the dashboard cannot load Alpaca paper data.
The BLITZ control submits a simulated market order to your Alpaca paper account. No real capital is involved. The button switches between a test buy while flat and a test sell while long.
Before you click it, which paper order side do you expect from the position shown on your dashboard?
- Click the visible BLITZ TEST BUY or BLITZ TEST SELL button once.
The activity panel should record a manual BUY or SELL. The position card should update after the next paper-account refresh.
- Confirm trades.csv appears in the Explorer sidebar after the simulated order is submitted.
- Confirm the dashboard remains responsive while its cards and charts update.
What does a paper fill prove?
The paper fill proves that your dashboard can route an order through the simulated account. It does not predict the price or timing of live execution.
Alpaca paper trading does not fully model market impact, latency slippage, queue position, price improvement, or regulatory fees. Keep every order in the paper environment throughout this project.
🙋♀️ BLITZ order not updating?
- Read the newest activity entry for the reported paper-order problem.
- Check the terminal for the corresponding error details.
- Confirm the dashboard status still shows a live connection before trying the button again.
Help me troubleshoot a failed simulated BLITZ order.
You did it. Your crossover strategy now runs inside a responsive paper-trading dashboard with live state, charts, simulated execution, and local trade records.
Secret mission
Switch to 30/70 RSI Bands
Your crossover engine already reads its strategy levels from private configuration. In this mission, you will switch to 30/70 RSI bands without changing a Python file, then prove the dashboard adopted them.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local project, pause it for later, or delete it entirely. This project costs $0 because every order and position in Alpaca Paper Trading is simulated.
Resources you used:
- The running local dashboard process.
- The algo-trading-bot folder, including .venv, .env, source files, and optional trades.csv.
- An optional simulated BTC/USD paper position.
Keep everything running
Keeping everything preserves your project files and any simulated paper position. Stop the dashboard while you are away so it does not keep polling Alpaca.
- In the dashboard from earlier, click STOP.
- Close the dashboard window.
- Leave any simulated paper position available for later practice.
- Keep .env inside algo-trading-bot private.
Your bot is parked for later practice. Its private credentials remain local.
Pause - I'll come back to this later
Pausing keeps every local file while stopping the polling process. You can also return your simulated paper account to a flat position.
Closing the position affects only simulated funds. Live capital stays untouched.
- In the dashboard from earlier, click STOP.
- Check the POSITION card.
- Click BLITZ TEST SELL if the card shows a quantity that you want to close.
- Wait for the POSITION card to show FLAT if you submitted the simulated sell.
- Close the dashboard window.
The polling process is stopped. Your project remains ready for another paper-trading session.
Delete - I don't want to use this again
Deleting the algo-trading-bot folder removes its private .env, .venv, source files, and optional trades.csv in one pass.
The position cleanup affects only simulated funds in your paper account. Live capital stays untouched.
- In the dashboard from earlier, click STOP.
- Check the POSITION card.
- Click BLITZ TEST SELL if the card shows a quantity.
- Wait for the POSITION card to show FLAT.
- Close the dashboard window.
- Close Visual Studio Code.
You should see FLAT before deleting the local files. This confirms the simulated position is closed.
The folder deletion steps differ by operating system.
macOS
- In Finder, navigate to the parent folder that contains algo-trading-bot.
- Move algo-trading-bot to the Trash.
- Empty the Trash.
- Search Finder for algo-trading-bot.
You should see no matching project folder. That confirms your private .env and local bot files are gone.
Windows
- In File Explorer, navigate to the parent folder that contains algo-trading-bot.
- Move algo-trading-bot to the Recycle Bin.
- Empty the Recycle Bin.
- Search File Explorer for algo-trading-bot.
You should see no matching project folder. That confirms your private .env and local bot files are gone.
Nice Work!
Nice Work!
You did it! Your Algo Trading Bot now turns BTC/USD candles from Alpaca into closed-candle RSI crossover signals inside a responsive paper-trading dashboard.
You've learned how to:
- Built a tested RSI crossover strategy that emits one action at each crossing. Its position-aware logic selects BUY, SELL, or HOLD.
- Retrieved BTC/USD candles plus quotes from Alpaca. Restricted strategy decisions to completed candles.
- Built a responsive Tkinter dashboard with embedded Matplotlib charts. Kept background polling outside the desktop event loop. Connected its controls to simulated Alpaca orders recorded in trades.csv.
- Secret Mission: Reconfigured the strategy with RSI_ENTRY=30 plus RSI_EXIT=70. Verified 30/70 crossover behavior without changing any Python files.
Ready to quiz yourself?