Skip to content
Back to App

Your First Algorithm

This tutorial walks you through building a complete trading algorithm from scratch. By the end, you will have a working strategy that places real orders, manages a position, and reports its performance — all in under 80 lines of Python.

What We Are Building

A simple interval-based strategy:

  • Buy every 100 bars
  • Hold for 50 bars, then sell
  • Track P&L and print results

It is not a profitable strategy — it is a learning exercise that teaches you how every part of the TestMax API works.

Understanding the Environment

Before writing any code, let’s understand what the Algo Playground gives you:

# These environment variables are pre-set by the Playground:
ACCOUNT_ID # Your simulated trading account
CONTRACT_ID # The futures contract (e.g., NQ, ES)
TOTAL_BARS # How many bars are in the backtest
STEP_DELAY # Delay between bars (controls playback speed)
SPEED_FILE # File path for reading speed from the UI slider

You also get these functions ready to use:

  • get_next_bar() — fetch the next OHLCV bar
  • place_order() — submit a buy or sell order
  • get_positions() — check what you currently hold
  • close_all_positions() — flatten everything
  • get_account() — get balance and account info
  • read_speed() — sync with the UI speed slider

Building the Strategy

Step 1: Set Up the Boilerplate

Every TestMax strategy starts with the same skeleton. This reads environment variables and sets up the main loop:

import os, time
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))
CONTRACT_ID = os.environ.get("CONTRACT_ID", "")
TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))
STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))
SPEED_FILE = os.environ.get("SPEED_FILE", "")
print(f"[INFO] Account: {ACCOUNT_ID}, Contract: {CONTRACT_ID}")
print(f"[INFO] Total Bars: {TOTAL_BARS}")
print("-" * 50)

This is the same for every strategy. Copy it once, reuse it forever.

Step 2: Build the Main Loop (Just Logging)

Before placing any orders, let’s just read bars and print them. This verifies everything is connected:

for i in range(TOTAL_BARS):
bar = get_next_bar()
if bar is None:
print("[INFO] No more bars")
break
read_speed()
if STEP_DELAY > 0:
time.sleep(STEP_DELAY)
# Print every 100th bar so we don't flood the console
if i % 100 == 0:
print(f"Bar {i}: O={bar['o']:.2f} H={bar['h']:.2f} L={bar['l']:.2f} C={bar['c']:.2f} V={bar['v']}")

Try it now. Paste this into the Playground, select any instrument and date, and run it. You should see bar data scrolling in the console.

Step 3: Add Position Tracking Variables

Now let’s add variables to track our position state:

position = None # None, 'LONG', or 'SHORT'
entry_bar = 0 # Which bar number we entered on
entry_price = 0.0 # The price we entered at
trade_count = 0 # Total trades taken
wins = 0 # Winning trades

Step 4: Add Buy Logic — Enter Every 100 Bars

Inside the main loop, after reading the bar, add the entry condition:

price = bar["c"] # Use the closing price
# Entry: buy every 100 bars if we have no position
if position is None and i % 100 == 0 and i > 0:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) # side=0 (Buy), size=1 contract
if result.get("orderId"):
position = "LONG"
entry_bar = i
entry_price = price
print(f"[BUY] Bar {i} @ {price:.2f}")

Let’s break down place_order:

  • ACCOUNT_ID — which account to trade on
  • CONTRACT_ID — which instrument
  • 0 — side: 0 = Buy, 1 = Sell
  • 1 — size: 1 contract

It returns a dict with orderId if the order was accepted.

Step 5: Add Sell Logic — Exit After 50 Bars

Now add the exit condition. If we have been in a position for 50 bars, sell:

# Exit: sell after holding for 50 bars
if position == "LONG" and (i - entry_bar) >= 50:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) # side=1 (Sell)
if result.get("orderId"):
pnl_points = price - entry_price
trade_count += 1
if pnl_points > 0:
wins += 1
print(f"[SELL] Bar {i} @ {price:.2f} | P&L: {pnl_points:+.2f} pts")
position = None

Step 6: Add Cleanup and Summary

After the main loop, always close any open positions and print a summary:

# Cleanup: close any remaining position
try:
close_all_positions(ACCOUNT_ID, CONTRACT_ID)
except:
pass
# Print summary
account = get_account(ACCOUNT_ID)
balance = account["balance"] if account else 50000
pnl = balance - 50000
print("-" * 50)
print(f"[DONE] Final Balance: ${balance:,.2f}")
print(f"[DONE] Net P&L: ${pnl:+,.2f}")
print(f"[DONE] Total Trades: {trade_count}")
if trade_count > 0:
print(f"[DONE] Win Rate: {wins / trade_count * 100:.1f}%")

Complete Code

Here is the entire strategy in one block. Copy and paste this into the Algo Playground:

#!/usr/bin/env python3
"""
My First Algorithm — TestMax Algo Playground
Buy every 100 bars, sell 50 bars later.
"""
import os, time
# --- Setup ---
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))
CONTRACT_ID = os.environ.get("CONTRACT_ID", "")
TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))
STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))
SPEED_FILE = os.environ.get("SPEED_FILE", "")
print(f"[INFO] Account: {ACCOUNT_ID}, Contract: {CONTRACT_ID}")
print(f"[INFO] Strategy: Buy every 100 bars, hold for 50")
print("-" * 50)
# --- State ---
position = None
entry_bar = 0
entry_price = 0.0
trade_count = 0
wins = 0
# --- Main Loop ---
try:
for i in range(TOTAL_BARS):
bar = get_next_bar()
if bar is None:
print("[INFO] No more bars")
break
read_speed()
if STEP_DELAY > 0:
time.sleep(STEP_DELAY)
price = bar["c"]
# Log progress every 500 bars
if i % 500 == 0:
print(f"[INFO] Bar {i}/{TOTAL_BARS} | Price: {price:.2f}")
# --- Entry: buy every 100 bars ---
if position is None and i % 100 == 0 and i > 0:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1)
if result.get("orderId"):
position = "LONG"
entry_bar = i
entry_price = price
print(f"[BUY] Bar {i} @ {price:.2f}")
# --- Exit: sell after 50 bars ---
if position == "LONG" and (i - entry_bar) >= 50:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1)
if result.get("orderId"):
pnl_points = price - entry_price
trade_count += 1
if pnl_points > 0:
wins += 1
print(f"[SELL] Bar {i} @ {price:.2f} | P&L: {pnl_points:+.2f} pts")
position = None
finally:
# Always clean up
try:
close_all_positions(ACCOUNT_ID, CONTRACT_ID)
except:
pass
account = get_account(ACCOUNT_ID)
balance = account["balance"] if account else 50000
pnl = balance - 50000
print("-" * 50)
print(f"[DONE] Final Balance: ${balance:,.2f}")
print(f"[DONE] Net P&L: ${pnl:+,.2f}")
print(f"[DONE] Total Trades: {trade_count}")
if trade_count > 0:
print(f"[DONE] Win Rate: {wins / trade_count * 100:.1f}%")

Key Takeaways

  1. get_next_bar() returns one OHLCV bar at a time. Your entire strategy runs inside a for loop that calls this on every iteration.

  2. place_order(account, contract, side, size) is how you trade. Side 0 = Buy, Side 1 = Sell. Always check the result for orderId to confirm it was accepted.

  3. Always call read_speed() inside your loop. This lets the UI speed slider control playback.

  4. Always wrap your loop in try/finally and call close_all_positions() in the finally block. This ensures positions are cleaned up even if your code crashes.

  5. Use get_account() at the end to get the actual balance. TestMax tracks the real P&L including slippage and fills — do not rely solely on your own calculations.

Exercises

Try modifying this strategy to learn more:

  1. Change the interval — try buying every 50 bars or every 200 bars. Does the result change?
  2. Add short selling — alternate between buying and selling. Use side=1 to go short.
  3. Add a simple filter — only buy if the current bar’s close is above its open (a green candle).
  4. Track more stats — calculate the average win and average loss in points.

What’s Next

This strategy trades blindly on a timer. In the next tutorials, you will learn to make decisions based on actual market data: