In the previous Phase 4 lessons, we studied a signal as a research event.
Signal
↓
Forward Return
↓
Baseline
↓
Distribution
↓
Dependence
↓
Uncertainty
But a real trading position behaves differently.
A signal may happen at one moment, while a position can remain open for many bars.
Phase 4-06 introduces that missing idea: the position lifecycle.
1. Event Study and Trading Position Are Not the Same Thing
Until now, an SMA20 Cross Above event was followed by a fixed future window:
Signal
after Close(t)
Measure
Open(t+1)
→
Open(t+6)
That is useful for asking:
What usually happened
after the signal?
But a trading strategy asks a different question:
When do we enter?
While we are already in,
what do we do?
When do we exit?
That requires a position state.
2. Start with Only Two States
We deliberately keep the first lifecycle simple.
CASH
or
LONG
No short selling. No leverage. No partial positions. No multiple entries.
One position at a time.
3. See the Lifecycle Before Writing Code
The whole model can be drawn as:
CASH
↓
Cross Above
after Close(t)
↓
BUY at Open(t+1)
↓
LONG
↓
Cross Below
after Close(t)
↓
SELL at Open(t+1)
↓
CASH
The Python program creates the same idea as a state-machine diagram.
4. Why Do We Need a State?
Suppose a Cross Above signal appears.
We buy and become LONG.
Two days later another bullish-looking event appears. Should we buy again?
In this first model:
No.
We are already LONG.
The correct action depends not only on the new event, but also on the position we already hold.
same event
+
different current state
=
different action
5. State and Event Have Different Meanings
A state can last for many bars.
LONG
LONG
LONG
LONG
LONG
An event happens at a transition.
Cross Above
happens once
then the position
may remain LONG
for many bars
Therefore:
State ≠ Event
6. Keep Signal Time and Execution Time Separate
Our SMA20 Cross Above uses the current Close.
That means the event is known only after the current bar closes.
Close(t)
↓
now we know
Cross Above happened
We cannot pretend that we already traded at that same Close.
The simple execution rule remains:
Signal
after Close(t)
Execution
Open(t+1)
7. Entry Rule
Entry requires both:
Current Position
=
CASH
AND
Cross Above
=
True
Then:
schedule BUY
execute at
next bar Open
8. Exit Rule
Exit also depends on the current position.
Current Position
=
LONG
AND
Cross Below
=
True
Then:
schedule SELL
execute at
next bar Open
9. Ignore Events That Do Not Match the Current State
Suppose we are already LONG and another Cross Above appears.
LONG
+
Cross Above
→
do nothing
Likewise:
CASH
+
Cross Below
→
do nothing
This is the first place where a state machine prevents contradictory trading actions.
10. See One Real Trade on Candlesticks
The program finds a real closed AAPL trade generated by the SMA20 lifecycle.
The chart shows:
Entry signal
after Close(t)
BUY
at next Open
LONG holding period
Exit signal
after Close(t)
SELL
at next Open
Green candles are bullish. Red candles are bearish.
The important idea is that the signal line and the execution line are not the same line.
11. Follow One Tiny Example by Hand
Imagine this sequence:
Day 1
CASH
Day 2 Close
Cross Above occurs
Day 3 Open
BUY at 102
Day 3
LONG
Day 4
LONG
Day 4 Close
Cross Below occurs
Day 5 Open
SELL at 104
Day 5
CASH
The trade return is:
104
---
102
- 1
=
+1.96%
Notice that the entry and exit signals happen one bar before the actual executions.
12. A Position Is a Value That Persists Through Time
We can encode the state numerically:
CASH
=
0
LONG
=
1
Then a sequence may look like:
Date Position
Day 1 0
Day 2 0
Day 3 1
Day 4 1
Day 5 0
Day 6 0
13. See the Position State as a Timeline
The program creates:
position_state_timeline.png
The line can only be at:
0
CASH
or
1
LONG
A jump from 0 to 1 means a BUY was executed. A drop from 1 to 0 means a SELL was executed.
14. Event, Execution, Position, and Trade Are Four Different Things
These terms are easy to mix together.
Event
Cross Above or Cross Below
detected after Close(t)
Execution
BUY or SELL
at Open(t+1)
Position
CASH or LONG
held through time
Trade
one completed
Entry → Exit pair
This distinction is fundamental for every backtest we build later.
15. Why Use a Pending Action?
When a signal appears after Close(t), the execution belongs to the next bar.
The code therefore temporarily stores:
pending_action
pending_signal_index
Example:
today's Close
Cross Above
↓
pending_action = BUY
next day's Open
execute BUY
↓
pending_action = None
This makes the timing explicit instead of hiding it inside a return formula.
16. What Does the Lifecycle Table Store?
The program saves:
position_lifecycle_table.csv
Each market row includes:
Open
High
Low
Close
SMA
Cross Above
Cross Below
Position State
Position
Execution Action
Execution Price
Triggering Signal Date
This table answers:
What happened
on every bar?
17. What Is a Trade Ledger?
The second output is:
position_trade_ledger.csv
One row represents one completed trade.
Entry Signal Date
Entry Date
Entry Price
Exit Signal Date
Exit Date
Exit Price
Bars Held
Gross Return
This table answers:
What happened
for every completed trade?
18. Lifecycle Table and Trade Ledger Are Different
Lifecycle Table
one row
per market bar
Trade Ledger
one row
per completed trade
We need both.
The lifecycle table explains position state through time. The trade ledger summarizes complete Entry → Exit episodes.
19. What Happens If the Last Trade Is Still Open?
The dataset may end while the position is still LONG.
last bar arrives
position
=
LONG
but no future
Cross Below yet
Phase 4-06 does not invent an artificial exit.
It reports:
OPEN AT END OF DATA
Later backtest rules can decide whether the final position should be force-closed for reporting.
20. Why Do We Call the Return “Gross Return”?
The trade ledger currently calculates:
Exit Price
----------
Entry Price
- 1
But it does not subtract:
commission
spread
slippage
Therefore the correct name is:
Gross Return
Costs will be added later in Phase 4-08.
21. Why Is This Still Not a Full Backtest?
We now know:
when a trade enters
when it exits
how long it stays open
its gross return
But we have not yet modeled:
starting capital
number of shares
portfolio value
cash balance
equity curve
drawdown
Those belong to Phase 4-07.
Therefore:
Position Lifecycle
≠
Full Backtest
22. The Complete Python Program
This lesson reuses:
FinanceDataReader
pandas
matplotlib
Save as:
phase4_06_position_lifecycle.py
from pathlib import Path
from datetime import date, timedelta
import os
import FinanceDataReader as fdr
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import pandas as pd
# ============================================================
# Phase 4-06 — Position Lifecycle
#
# Main idea:
#
# Event Study
# ↓
# Entry / Exit Events
# ↓
# Execution at Next Open
# ↓
# Position State
# ↓
# Trade Ledger
#
# State machine:
#
# CASH
# ↓ Cross Above signal after Close(t)
# BUY at Open(t+1)
# ↓
# LONG
# ↓ Cross Below signal after Close(t)
# SELL at Open(t+1)
# ↓
# CASH
#
# Important:
#
# Signal Time ≠ Execution Time
# Event ≠ Position
# Position ≠ Trade
# Event Study ≠ Backtest
#
# This lesson does NOT add:
# - fees
# - slippage
# - leverage
# - stop loss
# - position sizing
# - portfolio logic
#
# Change one thing at a time.
# ============================================================
# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------
symbol = "AAPL"
recent_trading_days = 800
sma_period = 20
SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)
# ------------------------------------------------------------
# 2. SMA
# ------------------------------------------------------------
def simple_moving_average(
values,
period,
):
result = [
None
] * len(values)
if period <= 0:
raise ValueError(
"period must be positive"
)
for i in range(
period - 1,
len(values),
):
window = values[
i - period + 1 : i + 1
]
result[i] = (
sum(
float(value)
for value
in window
)
/ period
)
return result
# ------------------------------------------------------------
# 3. State: Close above SMA?
# ------------------------------------------------------------
def above_sma_state(
close_values,
sma_values,
):
result = [
None
] * len(close_values)
for i in range(
len(close_values)
):
if sma_values[i] is None:
continue
result[i] = (
float(
close_values[i]
)
>
float(
sma_values[i]
)
)
return result
# ------------------------------------------------------------
# 4. Events: Cross Above / Cross Below
# ------------------------------------------------------------
def crossover_events(
above_state,
):
cross_above = [
False
] * len(above_state)
cross_below = [
False
] * len(above_state)
for i in range(
1,
len(above_state),
):
previous_state = (
above_state[i - 1]
)
current_state = (
above_state[i]
)
if (
previous_state is None
or current_state is None
):
continue
cross_above[i] = (
previous_state is False
and current_state is True
)
cross_below[i] = (
previous_state is True
and current_state is False
)
return (
cross_above,
cross_below,
)
# ------------------------------------------------------------
# 5. Position lifecycle engine
# ------------------------------------------------------------
def build_position_lifecycle(
market_df,
):
"""
Signal timing:
Cross Above / Cross Below is known
only after Close(t).
Execution:
BUY or SELL happens at Open(t+1).
Position states:
CASH
LONG
We use one position at a time.
If already LONG:
another Cross Above does nothing.
If already CASH:
a Cross Below does nothing.
"""
n = len(market_df)
position_state = [
"CASH"
] * n
execution_action = [
""
] * n
execution_price = [
None
] * n
triggering_signal_date = [
None
] * n
state = "CASH"
pending_action = None
pending_signal_index = None
open_trade = None
closed_trades = []
for i in range(n):
# ----------------------------------------
# A. Execute a previously scheduled order
# at today's Open.
# ----------------------------------------
if pending_action is not None:
price = float(
market_df.iloc[i]["Open"]
)
signal_date = (
market_df.index[
pending_signal_index
]
)
if pending_action == "BUY":
state = "LONG"
execution_action[i] = "BUY"
execution_price[i] = price
triggering_signal_date[i] = (
signal_date
)
open_trade = {
"Entry Signal Date":
signal_date,
"Entry Date":
market_df.index[i],
"Entry Price":
price,
"Entry Index":
i,
}
elif pending_action == "SELL":
execution_action[i] = "SELL"
execution_price[i] = price
triggering_signal_date[i] = (
signal_date
)
if open_trade is None:
raise RuntimeError(
"SELL execution found "
"without an open trade."
)
gross_return = (
price
/ open_trade[
"Entry Price"
]
- 1.0
)
bars_held = (
i
- open_trade[
"Entry Index"
]
)
closed_trades.append(
{
"Entry Signal Date":
open_trade[
"Entry Signal Date"
],
"Entry Date":
open_trade[
"Entry Date"
],
"Entry Price":
open_trade[
"Entry Price"
],
"Exit Signal Date":
signal_date,
"Exit Date":
market_df.index[i],
"Exit Price":
price,
"Bars Held":
bars_held,
"Gross Return":
gross_return,
}
)
open_trade = None
state = "CASH"
pending_action = None
pending_signal_index = None
# ----------------------------------------
# B. Record the position held during
# today's bar after the Open execution.
# ----------------------------------------
position_state[i] = state
# ----------------------------------------
# C. At today's Close, inspect the event.
#
# We cannot execute until next Open.
# ----------------------------------------
if i >= n - 1:
continue
cross_above = bool(
market_df.iloc[i][
"Cross Above"
]
)
cross_below = bool(
market_df.iloc[i][
"Cross Below"
]
)
if (
state == "CASH"
and cross_above
):
pending_action = "BUY"
pending_signal_index = i
elif (
state == "LONG"
and cross_below
):
pending_action = "SELL"
pending_signal_index = i
lifecycle_df = (
market_df.copy()
)
lifecycle_df[
"Position State"
] = position_state
lifecycle_df[
"Position"
] = [
1
if value == "LONG"
else 0
for value
in position_state
]
lifecycle_df[
"Execution Action"
] = execution_action
lifecycle_df[
"Execution Price"
] = execution_price
lifecycle_df[
"Triggering Signal Date"
] = triggering_signal_date
trade_df = pd.DataFrame(
closed_trades
)
open_trade_summary = None
if open_trade is not None:
open_trade_summary = {
"Status":
"OPEN AT END OF DATA",
"Entry Signal Date":
open_trade[
"Entry Signal Date"
],
"Entry Date":
open_trade[
"Entry Date"
],
"Entry Price":
open_trade[
"Entry Price"
],
}
return (
lifecycle_df,
trade_df,
open_trade_summary,
)
# ------------------------------------------------------------
# 6. Candlestick renderer
# ------------------------------------------------------------
def draw_candlesticks(
ax,
market_df,
body_width=0.62,
):
"""
Bullish candle:
seagreen
Bearish candle:
firebrick
"""
bullish_color = "seagreen"
bearish_color = "firebrick"
for x, (_, row) in enumerate(
market_df.iterrows()
):
open_price = float(
row["Open"]
)
high_price = float(
row["High"]
)
low_price = float(
row["Low"]
)
close_price = float(
row["Close"]
)
bullish = (
close_price
>= open_price
)
candle_color = (
bullish_color
if bullish
else bearish_color
)
ax.vlines(
x,
low_price,
high_price,
color=candle_color,
linewidth=1.0,
)
body_bottom = min(
open_price,
close_price,
)
body_height = abs(
close_price
- open_price
)
if body_height == 0:
body_height = max(
high_price
- low_price,
0.01,
) * 0.02
ax.add_patch(
Rectangle(
(
x
- body_width / 2.0,
body_bottom,
),
body_width,
body_height,
facecolor=candle_color,
edgecolor=candle_color,
linewidth=1.0,
alpha=0.90,
)
)
# ------------------------------------------------------------
# 7. Deterministic self-test
# ------------------------------------------------------------
toy_df = pd.DataFrame(
{
"Open": [
100,
101,
102,
103,
104,
105,
],
"High": [
101,
102,
103,
104,
105,
106,
],
"Low": [
99,
100,
101,
102,
103,
104,
],
"Close": [
100,
101,
102,
103,
104,
105,
],
"Cross Above": [
False,
True,
False,
False,
False,
False,
],
"Cross Below": [
False,
False,
False,
True,
False,
False,
],
},
index=pd.date_range(
"2026-01-01",
periods=6,
freq="D",
),
)
(
toy_lifecycle,
toy_trades,
toy_open_trade,
) = build_position_lifecycle(
toy_df
)
# Cross Above on row 1:
# BUY should execute at Open(row 2) = 102
assert (
toy_lifecycle.iloc[2][
"Execution Action"
]
== "BUY"
)
assert abs(
float(
toy_lifecycle.iloc[2][
"Execution Price"
]
)
- 102.0
) < 1e-12
# Cross Below on row 3:
# SELL should execute at Open(row 4) = 104
assert (
toy_lifecycle.iloc[4][
"Execution Action"
]
== "SELL"
)
assert abs(
float(
toy_lifecycle.iloc[4][
"Execution Price"
]
)
- 104.0
) < 1e-12
# Position should be LONG on rows 2 and 3.
assert (
toy_lifecycle.iloc[2][
"Position State"
]
== "LONG"
)
assert (
toy_lifecycle.iloc[3][
"Position State"
]
== "LONG"
)
# After SELL at row 4:
# position should be CASH.
assert (
toy_lifecycle.iloc[4][
"Position State"
]
== "CASH"
)
assert len(
toy_trades
) == 1
expected_return = (
104.0
/ 102.0
- 1.0
)
assert abs(
float(
toy_trades.iloc[0][
"Gross Return"
]
)
- expected_return
) < 1e-12
assert (
toy_open_trade
is None
)
print("Self-test")
print("=========")
print(
"BUY execution:",
toy_lifecycle.iloc[2][
"Execution Price"
],
)
print(
"SELL execution:",
toy_lifecycle.iloc[4][
"Execution Price"
],
)
print(
"Gross return:",
toy_trades.iloc[0][
"Gross Return"
],
)
print("Self-test: PASS")
print()
# ------------------------------------------------------------
# 8. Download market data
# ------------------------------------------------------------
today = date.today()
start_date = (
today
- timedelta(
days=1800
)
).strftime(
"%Y-%m-%d"
)
end_date = (
today.strftime(
"%Y-%m-%d"
)
)
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
df = df.tail(
recent_trading_days
).copy()
if len(df) < (
sma_period + 30
):
raise ValueError(
"Not enough market data."
)
# ------------------------------------------------------------
# 9. Build the frozen SMA20 events
# ------------------------------------------------------------
close_values = [
float(value)
for value
in df["Close"]
]
sma_values = (
simple_moving_average(
close_values,
sma_period,
)
)
state = above_sma_state(
close_values,
sma_values,
)
cross_above, cross_below = (
crossover_events(
state
)
)
df["SMA"] = sma_values
df["Above SMA"] = state
df["Cross Above"] = (
cross_above
)
df["Cross Below"] = (
cross_below
)
# ------------------------------------------------------------
# 10. Build the lifecycle
# ------------------------------------------------------------
(
lifecycle_df,
trades_df,
open_trade_summary,
) = build_position_lifecycle(
df
)
lifecycle_file = (
SCRIPT_DIR
/ "position_lifecycle_table.csv"
)
trade_file = (
SCRIPT_DIR
/ "position_trade_ledger.csv"
)
lifecycle_df.to_csv(
lifecycle_file
)
trades_df.to_csv(
trade_file,
index=False,
)
# ------------------------------------------------------------
# 11. Print lifecycle summary
# ------------------------------------------------------------
print("Position lifecycle")
print("==================")
print()
print(
f"Symbol: {symbol}"
)
print(
f"Closed trades: "
f"{len(trades_df)}"
)
print(
"Current position:",
lifecycle_df.iloc[-1][
"Position State"
],
)
print()
if not trades_df.empty:
display_trades = (
trades_df.copy()
)
display_trades[
"Gross Return"
] = (
100.0
* display_trades[
"Gross Return"
]
)
print(
"First closed trades"
)
print(
"-------------------"
)
print(
display_trades.head(
8
).to_string(
index=False
)
)
if (
open_trade_summary
is not None
):
print()
print(
"Open trade at end:"
)
print(
open_trade_summary
)
print()
print(
"Important:"
)
print(
"These are gross returns."
)
print(
"No fees or slippage "
"have been applied yet."
)
print(
"This lesson builds "
"position accounting, "
"not a full backtest."
)
# ------------------------------------------------------------
# 12. Visual 1 — state machine diagram
# ------------------------------------------------------------
state_machine_file = (
SCRIPT_DIR
/ "position_state_machine.png"
)
fig, ax = plt.subplots(
figsize=(10, 4.8)
)
ax.axis(
"off"
)
ax.text(
0.18,
0.55,
"CASH",
ha="center",
va="center",
fontsize=18,
bbox=dict(
boxstyle="round,pad=0.6",
fill=False,
),
)
ax.text(
0.50,
0.55,
"BUY\nOpen(t+1)",
ha="center",
va="center",
fontsize=14,
)
ax.text(
0.82,
0.55,
"LONG",
ha="center",
va="center",
fontsize=18,
bbox=dict(
boxstyle="round,pad=0.6",
fill=False,
),
)
ax.annotate(
"",
xy=(
0.42,
0.55,
),
xytext=(
0.28,
0.55,
),
arrowprops=dict(
arrowstyle="->",
linewidth=1.5,
),
)
ax.text(
0.35,
0.67,
"Cross Above\nknown after Close(t)",
ha="center",
va="center",
fontsize=11,
)
ax.annotate(
"",
xy=(
0.72,
0.55,
),
xytext=(
0.58,
0.55,
),
arrowprops=dict(
arrowstyle="->",
linewidth=1.5,
),
)
ax.annotate(
"",
xy=(
0.28,
0.30,
),
xytext=(
0.72,
0.30,
),
arrowprops=dict(
arrowstyle="->",
linewidth=1.5,
),
)
ax.text(
0.50,
0.19,
"Cross Below after Close(t)\n"
"→ SELL at Open(t+1)",
ha="center",
va="center",
fontsize=11,
)
ax.set_title(
"Position Lifecycle: CASH → LONG → CASH",
fontsize=16,
)
fig.subplots_adjust(
left=0.03,
right=0.97,
top=0.88,
bottom=0.05,
)
fig.savefig(
state_machine_file,
dpi=140,
)
plt.close(fig)
# ------------------------------------------------------------
# 13. Visual 2 — one real closed trade on candles
# ------------------------------------------------------------
trade_candlestick_file = (
SCRIPT_DIR
/ "position_lifecycle_candlestick.png"
)
trade_example_file = (
SCRIPT_DIR
/ "position_lifecycle_example.csv"
)
if not trades_df.empty:
example_trade = (
trades_df.iloc[-1]
)
entry_date = (
pd.Timestamp(
example_trade[
"Entry Date"
]
)
)
exit_date = (
pd.Timestamp(
example_trade[
"Exit Date"
]
)
)
entry_index = (
lifecycle_df.index.get_loc(
entry_date
)
)
exit_index = (
lifecycle_df.index.get_loc(
exit_date
)
)
entry_signal_date = (
pd.Timestamp(
example_trade[
"Entry Signal Date"
]
)
)
exit_signal_date = (
pd.Timestamp(
example_trade[
"Exit Signal Date"
]
)
)
entry_signal_index = (
lifecycle_df.index.get_loc(
entry_signal_date
)
)
exit_signal_index = (
lifecycle_df.index.get_loc(
exit_signal_date
)
)
plot_start = max(
0,
entry_signal_index - 10,
)
plot_end = min(
len(lifecycle_df),
exit_index + 10,
)
plot_df = (
lifecycle_df.iloc[
plot_start:plot_end
].copy()
)
entry_x = (
entry_index
- plot_start
)
exit_x = (
exit_index
- plot_start
)
entry_signal_x = (
entry_signal_index
- plot_start
)
exit_signal_x = (
exit_signal_index
- plot_start
)
fig, ax = plt.subplots(
figsize=(12, 7)
)
draw_candlesticks(
ax,
plot_df,
)
ax.plot(
list(
range(
len(plot_df)
)
),
plot_df[
"SMA"
],
linewidth=1.4,
label=f"SMA {sma_period}",
)
ax.axvline(
entry_signal_x,
linestyle=":",
linewidth=1.2,
label=(
"Entry signal "
"after Close(t)"
),
)
ax.axvline(
entry_x,
color="seagreen",
linewidth=1.6,
label="BUY at next Open",
)
ax.axvline(
exit_signal_x,
linestyle=":",
linewidth=1.2,
label=(
"Exit signal "
"after Close(t)"
),
)
ax.axvline(
exit_x,
color="firebrick",
linewidth=1.6,
label="SELL at next Open",
)
ax.axvspan(
entry_x,
exit_x,
alpha=0.08,
label="LONG position",
)
ax.set_title(
f"{symbol} — From Entry Event "
"to Position Lifecycle"
)
ax.set_ylabel(
"Price"
)
ax.grid(
axis="y",
alpha=0.20,
)
step = max(
1,
len(plot_df) // 8,
)
tick_positions = list(
range(
0,
len(plot_df),
step,
)
)
tick_labels = [
plot_df.index[i].strftime(
"%Y-%m-%d"
)
for i
in tick_positions
]
ax.set_xticks(
tick_positions
)
ax.set_xticklabels(
tick_labels,
rotation=35,
ha="right",
)
ax.legend()
fig.subplots_adjust(
left=0.09,
right=0.98,
top=0.90,
bottom=0.18,
)
fig.savefig(
trade_candlestick_file,
dpi=140,
)
plt.close(fig)
pd.DataFrame(
[
example_trade
]
).to_csv(
trade_example_file,
index=False,
)
# ------------------------------------------------------------
# 14. Visual 3 — position state through time
# ------------------------------------------------------------
position_timeline_file = (
SCRIPT_DIR
/ "position_state_timeline.png"
)
timeline_df = (
lifecycle_df.tail(
160
).copy()
)
fig, ax = plt.subplots(
figsize=(12, 5.5)
)
x_values = list(
range(
len(timeline_df)
)
)
ax.step(
x_values,
timeline_df[
"Position"
],
where="post",
linewidth=1.8,
)
ax.set_yticks(
[0, 1]
)
ax.set_yticklabels(
[
"CASH",
"LONG",
]
)
ax.set_ylim(
-0.15,
1.15,
)
ax.set_title(
f"{symbol} — Position State Through Time"
)
ax.set_xlabel(
"Trading Date"
)
ax.grid(
axis="x",
alpha=0.15,
)
step = max(
1,
len(
timeline_df
) // 8,
)
tick_positions = list(
range(
0,
len(timeline_df),
step,
)
)
tick_labels = [
timeline_df.index[i].strftime(
"%Y-%m-%d"
)
for i
in tick_positions
]
ax.set_xticks(
tick_positions
)
ax.set_xticklabels(
tick_labels,
rotation=35,
ha="right",
)
fig.subplots_adjust(
left=0.12,
right=0.98,
top=0.88,
bottom=0.20,
)
fig.savefig(
position_timeline_file,
dpi=140,
)
plt.close(fig)
# ------------------------------------------------------------
# 15. Finish
# ------------------------------------------------------------
print()
print("Files saved:")
print(lifecycle_file)
print(trade_file)
print(state_machine_file)
if not trades_df.empty:
print(trade_example_file)
print(trade_candlestick_file)
print(position_timeline_file)
23. Run the Program
python phase4_06_position_lifecycle.py
First confirm:
Self-test: PASS
Then inspect:
position_lifecycle_table.csv
position_trade_ledger.csv
position_state_machine.png
position_lifecycle_example.csv
position_lifecycle_candlestick.png
position_state_timeline.png
24. Research Checkpoint
Indicator
SMA20
Entry Event
Cross Above
Entry Signal Time
after Close(t)
Entry Execution
Open(t+1)
Exit Event
Cross Below
Exit Signal Time
after Close(t)
Exit Execution
Open(t+1)
Position States
CASH
LONG
Trade Model
one position at a time
Current Output
Lifecycle Table
Trade Ledger
Return
Gross Return
Not Yet
capital
position size
fees
slippage
equity curve
drawdown
25. Check Your Understanding
- An event and a position are not the same thing.
- A position state can persist for many bars.
- A Cross Above event matters only when the current state is CASH.
- A Cross Below event matters only when the current state is LONG.
- The signal is known after Close(t), but the execution happens at Open(t+1).
- A pending action keeps signal time and execution time separate.
- The lifecycle table stores one row per market bar.
- The trade ledger stores one row per completed Entry → Exit trade.
- An open trade at the end of the dataset should be reported explicitly.
- Gross Return does not include trading costs.
- A position lifecycle is necessary for a backtest, but it is not yet a full backtest.
26. Change One Thing Yourself
Keep the SMA20 signal and next-open execution frozen.
Change only the exit event.
Instead of:
Exit
Cross Below SMA20
try a simple fixed holding rule:
Exit
5 bars after entry
Then ask:
How does the
Position timeline change?
How does
Bars Held change?
How many trades
are completed?
Do entry events
occur while already LONG?
Do not compare profitability yet.
The goal is to understand how an exit rule changes the lifecycle.
27. What You Just Learned
Event
↓
schedule action
↓
next Open execution
↓
Position State
↓
hold through time
↓
Exit Event
↓
next Open execution
↓
Trade completed
↓
Trade Ledger
The key idea is:
Trading is not
a collection of isolated signals.
It is a sequence of
state transitions through time.
28. Where Do We Go Next?
We now have:
CASH
↓
BUY
↓
LONG
↓
SELL
↓
CASH
We also have completed trades.
The next question is:
If we start with
a fixed amount of money,
how does the account value
change through time?
Phase 4-07 will build the first full backtest:
Starting Capital
↓
Position
↓
Trade PnL
↓
Cash
↓
Equity
↓
Equity Curve
Sources and Further Reading
- Python documentation — More Control Flow Tools — background for the conditional logic used in the lifecycle state machine.
- pandas — DataFrame — reference for storing the bar-by-bar lifecycle table and trade ledger.
- Matplotlib — Axes.step — used to visualize the CASH/LONG position state through time.