Skip to content

Commit

Permalink
Improve README, add 'Getting started' guide
Browse files Browse the repository at this point in the history
and use composeStrategy as the default method for
writing trading strategies
  • Loading branch information
pekam committed Jul 4, 2023
1 parent 64e69a7 commit 9a37f86
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 16 deletions.
180 changes: 164 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,176 @@

Ameba-TS is a multi-asset backtester for TypeScript.

## Backtest example
## Getting started

See [example.ts](src/example.ts) for an example of writing a trading strategy
and testing it with historical price data.
### Creating a trading strategy

The easiest way of writing a trading strategy is composing it from reusable
building blocks ("strategy components") with `composeStrategy`. You can provide
a set of entry filters (conditions which need to pass before a trade can be
entered), the method to enter trades, and one or many methods to exit the trade.

In the following example, a long position is entered with a market order when
the 50-period SMA (simple moving average) is above the 200-period SMA and the
20-period RSI (relative strength index) indicator is below 30. Both the stop
loss and take profit order are place 5 times the 20-period ATR (average true
range) away from the entry price, so the trades have 1-to-1 risk-to-reward
ratio.

<!-- prettier-ignore -->
```ts
const myStrategy: TradingStrategy = composeStrategy({
filters: [
gt(sma(50), sma(200)),
lt(rsi(20), 30)
],
entry: marketBuyEntry,
exits: [
atrStopLoss(20, 5),
atrTakeProfit(20, 5)
],
});
```

### Position sizing

To decide how many units to buy when entering a position (or sell when
shorting), we need to create a position sizing strategy, a `Staker`. For common
use cases this can be achieved as follows:

```ts
const myStaker: Staker = createStaker({
maxRelativeRisk: 0.01, // Risk 1% of account per trade
maxRelativeExposure: 1.5, // Use max 50% leverage for all positions in total
allowFractions: true, // Currencies and cryptos allow non-whole-number position sizes, stocks don't
});
```

### Providing data for the backtest

In order to test the strategy with historical price data, you need to implement
a data provider:

```ts
const myDataProvider: CandleDataProvider = {
name: "broker-name",
getCandles: async ({ symbol, timeframe, from, to }): Promise<Candle[]> => {
throw Error("Not implemented.");
},
};
```

You can implement the `getCandles` function e.g. by fetching from your broker's
API or by reading from your locally stored data set.

### Backtesting

Below is an example of testing how the strategy would have performed during the
year 2021 when trading BTC and ETH cryptocurrencies (the symbols are specific to
the data provider).

```ts
const result: BacktestResult = await backtest({
strategy: withStaker(myStrategy, myStaker),
dataProvider: myDataProvider,
initialBalance: 10000,
symbols: ["BTC", "ETH"],
timeframe: "1h",
from: "2021-01-01",
to: "2022-01-01",
});

console.log(result.stats);
```

The result tells us how much profit the strategy would have made, among other
statistics. You can obtain all of the executed trades in `result.trades`.

NOTE: The asynchronous `backtest` function loads price data on demand in batches
and clears old data from memory. To keep your code synchronous, you also have
the option to load the used data set into memory beforehand and use the
`backtestSync` function instead.

## Trading strategy architecture

### Powerful core
A trading strategy is a function that receives the current state of things and
returns the changes it wishes to make to the open orders. The state includes
e.g. the historical price data up to the current moment as `Candles` (OHLC data)
and the currently active positions and orders. The strategy function is called
each time when a new candle is completed, and the changes returned by the
strategy are activated by closing previous orders and/or opening new ones.

There are three layers of abstraction.

### 1. Using `composeStrategy` with a `Staker`

This allows you to focus on high level trading ideas and decouple trading
patterns from position sizing. See the 'Getting started' guide for an example.

Using `composeStrategy` enables easily experimenting with different kinds of
strategy components. Changes such as adding a new entry filter or changing the
entry method to limit order at the recent lows require changing only one line of
code. Also, if you create your custom strategy component, you can reuse it in
multiple strategies without repeating yourself.

### 2. Writing a `TradingStrategy` function

The backtester works with `FullTradingStrategy` function type. This strategy needs to take care of position
sizing and simultaneosly handle multiple tradable assets if provided. Writing a `FullTradingStrategy`
gives you the most control over the trading decisions, at the cost of complexity.
You can also write out a `TradingStrategy` function without utilizing
`composeStrategy`. You can still focus on one asset at a time and decouple
position sizing from trading patterns by using a `Staker`, but the code is more
verbose and harder to maintain.

### Easy-to-use abstraction
Below is an example of writing out the same strategy as composed in the 'Getting
started' guide:

Algo traders can often make a couple of simplifications to the trading process:
```ts
const myStrategy: TradingStrategy = (state: AssetState) => {
const fastSma = getSma(state, 50);
const slowSma = getSma(state, 200);
const rsi = getRsi(state, 20);
const atr = getAtr(state, 20);
if (
fastSma === undefined ||
slowSma === undefined ||
rsi === undefined ||
atr === undefined
) {
// More data needed before the indicators are ready
return {};
}
if (!state.position) {
if (fastSma > slowSma && rsi < 30) {
// Not in a position and entry conditions fulfilled
const currentPrice = state.series[state.series.length - 1].close;
return {
entryOrder: {
side: "buy",
type: "market",
},
// Exit orders set while not in a position will be posted immediately
// if/when the entry is filled
stopLoss: currentPrice - atr * 5,
takeProfit: currentPrice + atr * 5,
};
} else {
// Cancel potentially open entry order (although not really meaningful
// when the entry is a market order that should fill immediately)
return {
entryOrder: null,
};
}
} else {
// Here we could manage the exit orders while in a position
return {};
}
};
```

- Use the same trading patterns for any asset (if trading multiple assets)
- Decouple trading patterns from position sizing / risk management
### 3. `FullTradingStrategy`

By implementing a `TradingStrategy`, you can focus on trading one asset at a time based on technical analysis
without thinking about position sizes. In this case you need a separate `Staker` to handle position sizing,
but the provided `createStaker` function should cover the common use cases, such as
"I want to risk 1% of my account balance per trade." A `TradingStrategy` and a `Staker` can then be combined
into a `FullTradingStrategy` that can be backtested.
The backtester actually takes in a `FullTradingStrategy` function type
(`withStaker` returns `FullTradingStrategy`). This is the lowest level of
abstraction among the trading strategy APIs. If writing out a
`FullTradingStrategy`, you need to take care of position sizing and
simultaneosly handle multiple tradable assets (if provided). This gives you the
most control over the trading decisions, at the cost of complexity.
5 changes: 5 additions & 0 deletions src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
withStaker,
} from "./";

/*
TODO: Update to use composeStrategy as that is the recommended method for
writing strategies.
*/

/**
* A strategy that buys the breakout of previous candle's high if the price is
* above the 50-period simple moving average (SMA), aims to take a 5% profit and
Expand Down

0 comments on commit 9a37f86

Please sign in to comment.