Python basics through trading examples
Variables, numbers, text, lists, loops, conditions and functions, learned by building a position-size calculator and running it over a list of trade setups.
In this lesson you will
- Store values in variables and recognise Python's basic types.
- Work with lists of trade results using indexing, loops and conditions.
- Write a function with inputs, a docstring and a return value.
- Build a position-size calculator and apply it to several setups at once.
This lesson teaches the core of Python through calculations you already do as a trader. By the end, you will have written a position-size calculator and used it on several setups at once. Every concept is explained the first time it appears, so no programming experience is needed. Type the examples rather than pasting them if you can; it is the quickest way to learn the syntax.
Variables and types
A variable is a name that refers to a value. You create one with =, which in Python means "store", not "equals".
account_size = 25_000 # an integer: a whole number
risk_percent = 0.01 # a float: a number with a decimal point
symbol = "DEMO" # a string: text inside quotes
is_long = True # a boolean: True or False
risk_dollars = account_size * risk_percent
print(symbol, is_long, risk_dollars)
print(type(account_size), type(risk_percent), type(symbol), type(is_long))
print(f"Risking ${risk_dollars:,.2f} per trade on {symbol}")DEMO True 250.0
<class 'int'> <class 'float'> <class 'str'> <class 'bool'>
Risking $250.00 per trade on DEMOEverything after # on a line is a comment. Python ignores it; it is there for the reader.
The four types here cover most trading scripts. An int is a whole number, such as a share count. A float is a number with a decimal point, such as a price or a percentage. A str (string) is text. A bool (boolean) is True or False, the answer to a yes/no question. The built-in type() function tells you which type a value has.
Multiplying an int by a float gives a float, which is why risk_dollars prints as 250.0. The last line is an f-string, which you met in the previous lesson. Inside the braces, :,.2f is a format: , adds thousands separators and .2f shows two decimal places. The $ before the brace is just a dollar sign in the text.
Lists and loops
A list holds several values in order, inside square brackets. Here it holds the results of eight trades, in R.
trades_r = [1.8, -1.0, -1.0, 2.4, -0.5, 3.1, -1.0, 0.9] # each trade's result in R
print("Number of trades:", len(trades_r))
print("First trade:", trades_r[0], "| last trade:", trades_r[-1])
print("Total:", round(sum(trades_r), 2), "R")
wins = 0
for r in trades_r:
if r > 0:
wins = wins + 1
print(f"Win rate: {wins / len(trades_r):.0%}")Number of trades: 8
First trade: 1.8 | last trade: 0.9
Total: 4.7 R
Win rate: 50%len() counts the items. Square brackets after a list pick out one item by its position, called its index. Python counts from zero, so trades_r[0] is the first trade. Negative indexes count from the end: trades_r[-1] is the last. sum() adds the items, and round(x, 2) rounds to two decimal places.
The loop is the important part. for r in trades_r: means "for each item in the list, call it r and run the indented lines below". Indentation is how Python knows which lines belong to the loop: four spaces is the convention. Inside, if r > 0: runs its own indented line only when the condition is true. So the loop visits every trade and adds one to wins for each winner.
After the loop, wins / len(trades_r) is 0.5, and the format :.0% shows it as a percentage with no decimals.
Functions: a position-size calculator
A function is a named, reusable piece of code. You give it inputs, called arguments, and it gives back a result. Here is the calculation behind fixed-fractional position sizing: risk a set percentage of the account, and let the distance to the stop decide how many shares that buys.
def position_size(account, risk_pct, entry, stop):
"""Shares to buy so that a stop-out loses risk_pct of the account."""
risk_dollars = account * risk_pct
risk_per_share = abs(entry - stop)
if risk_per_share == 0:
raise ValueError("entry and stop cannot be the same price")
return int(risk_dollars // risk_per_share)
shares = position_size(account=25_000, risk_pct=0.01, entry=50.00, stop=48.50)
print("Shares:", shares)
print(f"Position value: ${shares * 50.00:,.2f}")
print(f"Loss if stopped: ${shares * 1.50:,.2f}")Shares: 166
Position value: $8,300.00
Loss if stopped: $249.00def starts a function definition: its name, then its arguments in brackets, then a colon. The indented lines are the body. The text in triple quotes is a docstring, a short description that editors show when you use the function.
The highlighted lines do the work. The account risks 1% of 25,000, which is 250. The stop is 1.50 below the entry; abs() takes the absolute value, so the same function works for short positions where the stop is above the entry. Then // divides and rounds down: 250 / 1.50 is 166.67, and you cannot buy two-thirds of a share, so the answer is 166. Rounding down means the real risk, 249, never exceeds the plan. int() turns the result into a whole number, and return hands it back to whoever called the function.
The if check guards against a stop at the entry price, which would mean dividing by zero. raise ValueError(...) stops the program with a clear message instead of producing a meaningless number. Failing loudly is a good habit in trading code: a silent wrong answer can turn into a real order.
When calling the function, writing account=25_000 rather than just 25_000 is optional, but it makes each number's meaning obvious.
Putting it together: several setups at once
The value of a function is that the same rule runs identically on every input. This example sizes three hypothetical setups. Each setup is a dictionary: a set of named values inside curly braces, where setup["entry"] looks up the value stored under the key "entry".
def position_size(account, risk_pct, entry, stop):
"""Shares to buy so that a stop-out loses risk_pct of the account."""
risk_dollars = account * risk_pct
risk_per_share = abs(entry - stop)
if risk_per_share == 0:
raise ValueError("entry and stop cannot be the same price")
return int(risk_dollars // risk_per_share)
account = 25_000
setups = [
{"symbol": "AAA", "entry": 50.00, "stop": 48.50},
{"symbol": "BBB", "entry": 120.00, "stop": 114.00},
{"symbol": "CCC", "entry": 8.40, "stop": 8.10},
]
for setup in setups:
shares = position_size(account, 0.01, setup["entry"], setup["stop"])
exposure = shares * setup["entry"]
print(f"{setup['symbol']}: {shares:>4} shares ${exposure:>9,.2f} position {exposure / account:.0%} of the account")AAA: 166 shares $ 8,300.00 position 33% of the account
BBB: 41 shares $ 4,920.00 position 20% of the account
CCC: 833 shares $ 6,997.20 position 28% of the accountThe function is repeated at the top so the example runs on its own; in a real project you would define it once and reuse it. In the format codes, >4 and >9 right-align a value in a space four or nine characters wide, which lines up the columns.
Look at the result rather than the code for a moment. Every setup risks the same 250, yet the positions range from 20% to 33% of the account, because the stop distances differ. This is risk-based sizing doing its job: the risk per trade stays constant while position size adapts. It also shows why a cap on position value, the subject of this lesson's exercise, is worth having when a stop is very tight.
What you have learned
In a few dozen lines you have used most of the building blocks that later modules rely on: variables, types, lists, loops, conditions, dictionaries and functions. Real price data will not come as short lists typed by hand, though. The next lesson introduces pandas, which handles thousands of rows of dated prices as a single table.
Key takeaways
- A variable is a name for a value. Integers, floats, strings and booleans cover most of what a trading script needs.
- Lists hold sequences such as trade results, and a for loop processes each item in turn.
- A function packages a calculation under a name, so the same rule is applied the same way every time.
- Position sizing from account size, risk percentage and stop distance is a few lines of code, and code applies it without exceptions.
Extend the position-size calculator
Add a max_position_pct argument to position_size that caps the position value at a share of the account, for example 25%, and returns the smaller of the two share counts. Run it on the three setups from this lesson and check by hand which ones are capped. Then make the function raise a clear error if risk_pct is above 0.05.
Self-check
Answer in your own words first, then reveal the answer.
In the list
trades_r = [1.8, -1.0, -1.0, 2.4], what aretrades_r[0]andtrades_r[-1]?Show answerHide answer
trades_r[0]is 1.8, the first item, because Python counts from zero.trades_r[-1]is 2.4: negative indexes count from the end, so -1 is the last item.Why does
position_sizeuse//andint()rather than ordinary division?Show answerHide answer
You cannot buy a fraction of a share in most markets.
//divides and rounds down to a whole number, so the actual risk never exceeds the planned risk, andint()turns the result into an integer.What happens if
position_sizeis called with the same entry and stop price, and why is that better than returning a number?Show answerHide answer
It raises a
ValueErrorwith a message. A zero stop distance would mean dividing by zero, or an unlimited position. Stopping loudly with a clear message is safer than silently returning a meaningless or enormous size.
