Coding a coin-flip trading strategy for the lulz
Coin-Flip Bitcoin scalp trading.
Whilst participating in a Discord server about trading, someone brought up that trading crypto is like gambling. Jokingly, I replied that to decide whether I opened a long or a short position, I just tossed a coin and went long if it was heads or short if it was tails. Following this, I've decided to code a simple implementation to test this strategy and see how it would actually perform.
This experiment is that it’s purely for FUN. I don’t recommend anyone use this as a serious trading strategy.
Scalping
Scalping, in its conventional form, is a technique that involves making numerous trades within a single day to capitalize on small price movements. This will help the trading robot as it can capitalize gains regardless of the overall market direction. It's typically done with high precision, using technical analysis and quick decision-making. In contrast, the coin-flip strategy removes all analysis and decision-making, relying entirely on randomness.
Setting up the experiment
First, we need to define the parameters of our experiment. We’ll use a random number generator to simulate the coin flip. For simplicity, we’ll assume a 50-50 chance of either heads or tails. The trades will be executed at the start of each new candle on a 5-minute chart, and we’ll hold the position until the next flip. Our dataset will be a historical price feed for a volatile cryptocurrency, like Bitcoin, over a specific period. Next, we’ll outline the rules for our trades:
- If the coin flip results in heads, we open a long position.
- If the coin flip results in tails, we open a short position.
- We close the previous position before opening a new one.
- We’ll also track our performance by recording:
- The entry and exit prices of each trade.
- The net profit or loss after each trade.
- The total accumulated profit or loss over the entire period.
Starting a prototype
I like lists, so here is a list of things that the program needs to do:
- Generate a random number (0 or 1)
- Get market data (candle format)
- Trading
- Risk management -- i know, ironic.
- Voilà!
1. Random int (0 or 1)
int8_t coin_flip()
{
// LONG = 1
// SHORT = -1
uint8_t bit = rand() & 1;
return bit == 0 ? 1 : -1;
}
To test the randomness I ran a while loop to generate 10k random iterations to hopefully get a long to short ratio of 1.
LONGS: 5067
SHORTS: 4933
RATIO: 1.027164
2. Reading candle data
In order to track the bot's performance I need to be able to get the current market data. Since I'm using 1 minute dataframes, I will consider the market buy price to be the current minutes close price and the sell price to be the next minutes open price. For simplicity I will not consider the highest and lowest prices of the candle frame even though that's where we can make most money in scalping.
struct candle_t {
char date[30];
long double open;
long double close;
long double high;
long double low;
};
char line[MAX_LINE_LENGTH];
struct candle_t *CANDLE_DATA = malloc(DATA_FRAMES * sizeof(struct candle_t));
if (CANDLE_DATA == NULL) {
fprintf(stderr, "Failed to allocate memory for CANDLE_DATA.\n");
return 1;
}
uint32_t counter = 0;
// Read candle data from stdin
while (fgets(line, sizeof(line), stdin)) {
char date[30];
long double open, close, high, low;
if (sscanf(line, "%29[^,],%Lf,%Lf,%Lf,%Lf", date, &open, &close, &high, &low) == 5) {
struct candle_t candle = {.open = open, .close = close, .high = high, .low = low};
strncpy(candle.date, date, sizeof(candle.date) - 1);
candle.date[sizeof(candle.date) - 1] = '\0'; // Ensure null-termination
CANDLE_DATA[counter] = candle;
counter += 1;
if (counter >= DATA_FRAMES) {
fprintf(stderr, "Reached parsing maximum data frames: %d.\n", DATA_FRAMES);
break;
}
} else {
fprintf(stderr, "Error parsing line: %s", line);
}
}
#ifdef DEBUG
printf("Parsed %d data frames.\n", counter);
#endif
The data format is as follows where the prices are in US Dollars:
date | open | close | high | low |
---|---|---|---|---|
2024-01-01T00:01 | 42273.43194 | 42289.2743124 | 42289.2743 | 42250.8773552 |
2024-01-01T00:02 | 42289.30430 | 42311.9492018 | 42313.2288 | 42289.3043058 |
2024-01-01T00:03 | 42311.94920 | 42319.0145084 | 42322.1238 | 42310.6995978 |
2024-01-01T00:04 | 42319.57443 | 42362.0584814 | 42362.0584 | 42319.1411765 |
The plot of the first 500 1min dataframes where you can observe the bitcoin price on the y-axis and the time on the x-axis:
3. Opening positions
The next step is to be able store and open positions which will simulate the trades. For this we need the following data:
struct position_t {
char open_date[30]; // When did we start the trade
char close_date[30]; // When did we close the trade
long double open; // At which price did we buy bitcoin
long double close; // At which price did we sell
uint8_t leverage; // The multiplier of money we are borrowing (ex: if we have $100 with x10 leverage we are trading with a volume of $1000)
uint32_t size; // Capital designed for the trade
int8_t direction; // Long or Short
long double result; // Money gained or lost in the trade
};
Having defined the structure I will define some bare logic to close any existing trade, fill in its missing data and open a new trade after 3 minutes. Whilst developing I'm feeding it 500 data frames, which means I should only make 500/3 trades = ~166 trades.
long double capital = BALANCE;
uint8_t LEVERAGE = 20;
uint32_t last_open_idx = 0;
uint16_t wins = 0;
uint16_t zero = 0;
for (uint32_t idx = 0; idx < counter; idx++) {
if (idx % 3 != 0 && !(idx + 1 > counter)) continue; // Temporary logic for opening trades.
if (idx != 0) { // Close any open trades
struct position_t *pos = &POSITIONS[last_open_idx];
strncpy(pos->close_date, CANDLE_DATA[idx].date, sizeof(pos->close_date) - 1);
pos->close_date[sizeof(pos->close_date) - 1] = '\0'; // Ensure null-termination
pos->close = CANDLE_DATA[idx].open;
pos->result = pos->size * pos->direction * pos->leverage * fabsl(pos->open - pos->close);
capital += pos->result;
#ifndef DEBUG
printf("%s,%s,%Lf,%Lf,%d,%d,%d,%Lf,%Lf\n", pos->open_date, pos->close_date, pos->open, pos->close, pos->leverage, pos->size, pos->direction, pos->result, capital);
#endif
if (pos->result > 0) wins += 1;
else if (pos->result == 0) zero += 1;
last_open_idx += 1;
if (idx + 1 > counter) break;
}
if (capital <= 0) {
fprintf(stderr, "Out of money.\n");
break;
}
struct position_t pos = {
.open_date = "",
.close_date = "",
.open = CANDLE_DATA[idx].close,
.close = 0,
.leverage = LEVERAGE,
.size = capital, // This should never be negative.
.direction = coin_flip(),
.result = 0
};
strncpy(pos.open_date, CANDLE_DATA[idx].date, sizeof(pos.open_date) - 1);
pos.open_date[sizeof(pos.open_date) - 1] = '\0'; // Ensure null-termination
POSITIONS[last_open_idx] = pos;
}
#ifdef DEBUG
uint16_t losses = last_open_idx - wins - zero;
long double ratio = (long double)wins / (long double)losses;
printf("Results:\nWins: %d\nLosses: %d\nZero: %d\nW/L Ratio: %Lf\nInitial capital: %d\nFinal capital: %Lf\n", wins, losses, zero, ratio, BALANCE, capital);
#endif
After some attempts which ended in Out of money
I finally got one trading session which
was fully able to finish and with benefits! Of course, this result is misleading as for me
to get here I had to run the program multiple times just to get an outcome I liked and in
reality you would probably loose all your money really quickly.
Results:
Wins: 89
Losses: 74
Zero: 3
W/L Ratio: 1.202703
Initial capital: 1000
Final capital: 7443.281486
Here is another example where the program fully ran but ended up taking a loss:
Results:
Wins: 78
Losses: 85
Zero: 3
W/L Ratio: 0.917647
Initial capital: 1000
Final capital: 957.402878
4. Improving the position opening logic with risk management
Right now when we start a trade we allocate 100% of our capital. This means that if there is a big price movement against our direction we will very quickly loose everything. To improve this we should implement a simple TP & SL solution as well as a capital allocation based on our risk tolerance.
For the capital allowance I will set a risk factor which will determine the % of available
capital to start a new trade. A simple .size = capital * RISK_FACTOR
will suffice
and I have initialised the risk factor to 0.1.
The next step is to add TP & SL logic to open new trades. To achieve this I will delete the current "logic" and add checks to close the trades. Because this is a high risk strategy I will set the take profit to +10% of the position and the SL to -5%.
- if (idx % 3 != 0 && !(idx + 1 > counter)) continue; // Temporary logic for opening trades.
+
if (open_trade) { // Close any open trades.
struct position_t *pos = &POSITIONS[last_open_idx];
long double current = CANDLE_DATA[idx].open;
pos->result =
pos->size * pos->direction * pos->leverage * (pos->open - current);
if (pos->result >= pos->size * TP || pos->result <= pos->size * SL) {
capital += pos->result + pos->size;
pos->close = current;
strncpy(pos->close_date, CANDLE_DATA[idx].date,
sizeof(pos->close_date) - 1);
pos->close_date[sizeof(pos->close_date) - 1] =
'\0'; // Ensure null-termination
#ifndef DEBUG
printf("%s,%s,%Lf,%Lf,%d,%d,%d,%Lf,%Lf\n", pos->open_date,
pos->close_date, pos->open, pos->close, pos->leverage, pos->size,
pos->direction, pos->result, capital);
#endif
if (pos->result > 0)
wins += 1;
else if (pos->result == 0)
zero += 1;
open_trade = false;
last_open_idx += 1;
}
if (idx + 1 > counter)
break;
continue;
}
With an initial capital of $1.000, the first run was able to perform 9 trades before reaching out of money. The biggest win was of $725.732.039,64 with an initial position of $2.339.062,91 which is a 310x. However, the biggest loss was of 128x the capital. The loss is so massive than it messes up the proportions of the plot.
After removing the biggest outliers, we can better see the result & capital progression:
open date | close date | open | close | leverage | size | direction | result | total capital |
---|---|---|---|---|---|---|---|---|
2024-01-01T00:01 | 2024-01-01T00:02 | 42289.274312 | 42289.304306 | 10 | 100.000000 | 1 | -29.993400 | 970.006600 |
2024-01-01T00:03 | 2024-01-01T00:04 | 42319.014508 | 42319.574430 | 10 | 97.000660 | -1 | 543.127647 | 1513.134247 |
2024-01-01T00:05 | 2024-01-01T00:07 | 42390.860417 | 42399.529117 | 10 | 151.313425 | -1 | 13116.906095 | 14630.040342 |
2024-01-01T00:08 | 2024-01-01T00:09 | 42411.915132 | 42414.984948 | 10 | 1463.004034 | -1 | 44911.528997 | 59541.569339 |
2024-01-01T00:10 | 2024-01-01T00:11 | 42410.394100 | 42410.384100 | 10 | 5954.156934 | 1 | 595.421648 | 60136.990986 |
2024-01-01T00:12 | 2024-01-01T00:14 | 42480.584379 | 42464.363112 | 10 | 6013.699099 | 1 | 975498.205407 | 1035635.196393 |
2024-01-01T00:15 | 2024-01-01T00:17 | 42489.689520 | 42511.275300 | 10 | 103563.519639 | -1 | 22354993.923849 | 23390629.120242 |
2024-01-01T00:18 | 2024-01-01T00:20 | 42499.959731 | 42468.933116 | 10 | 2339062.912024 | 1 | 725732039.643415 | 749122668.763657 |
2024-01-01T00:21 | 2024-01-01T00:23 | 42454.629477 | 42441.765399 | 10 | 74912266.876366 | -1 | -9636772442.543852 | -8887649773.780195 |
Other runs:
Reached parsing maximum data frames: 301530.
2024-01-01T00:01:00.000Z,2024-01-01T00:02:00.000Z,42289.274312,42289.304306,10.000000,100.000000,1.000000,-29.993400,970.006600
2024-01-01T00:03:00.000Z,2024-01-01T00:04:00.000Z,42319.014508,42319.574430,10.000000,97.000660,1.000000,-543.127647,426.878953
2024-01-01T00:05:00.000Z,2024-01-01T00:06:00.000Z,42390.860417,42390.870416,10.000000,42.687895,1.000000,-4.268149,422.610803
2024-01-01T00:07:00.000Z,2024-01-01T00:08:00.000Z,42418.010647,42418.434887,10.000000,42.261080,1.000000,-179.288196,243.322607
2024-01-01T00:09:00.000Z,2024-01-01T00:10:00.000Z,42419.221599,42420.494201,10.000000,24.332261,1.000000,-309.652861,-66.330254
Out of money.
5. Volià
In conclusion, while you could make a ton of money with leveraged trading making random decisions, it's also very likely you'll just burn through your account. In average, only 6 trades could be made before going below 0. In addition to this, the bot doesn't consider any fees or spread, so in reality the wins would be significantly lower. Also, because of the leveraged trades, the negative final balance is debt, so the situation is much worse.
You can find the whole code and data here: https://git.sr.ht/~shadowfax/coin_flip
Until the next one :^)