shadowfax

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:

Starting a prototype

I like lists, so here is a list of things that the program needs to do:

  1. Generate a random number (0 or 1)
  2. Get market data (candle format)
  3. Trading
  4. Risk management -- i know, ironic.
  5. 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:

data plot

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.

data plot

After removing the biggest outliers, we can better see the result & capital progression:

data plot

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.

data plot

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 :^)