Bitcoin Buying and Selling

Author

Dr. David Kirkby drkirkby@kirkbymicrowave.co.uk

Abtract

Dollar cost averaging (DCA) is a popular strategy for buying assets like bitcoin or shares, where a fixed amount of money is spent each day, irrespective of the price of the asset. When the asset falls in price, more can be purchased for the same amount of money. It is common to buy daily, weekly or monthly.

A strategy was implemented to buy bitcoin every day on the the Binance cryptocurrency exchange. Rather than buy at a fixed time each day, the time of purchasing was chosen in what was believed might allow one to accumulated more bitcoin for the same outlay.

Initially an algorithm was though of, that checked the bitcoin price each minute, and made a decision whether to buy or not. The price every minute was stored for a period of March to June 2026, giving approximately 96,000 observations of price.

Method

Initial statergy, based on a gut feeling, rather than any statistical analysis.

The work started by crating a Python script, with the aid of ChatGPT, which used the Binance API and bought BTC once ever calandar day. The strategy was to execute a script every minute of every day using cron on a Debian server. The script determined.

  1. The highest price in the last 24 hours, high_24h
  2. The loweest price in the last 24 hours, low_24h
  3. The current price, current_price
  4. A trigger price was calculated:

trigger_price = low_24h + k × (high_24h - low_24h)

where ‘k=0.15’, which corresponds to a price 15% of the way between the 24-hour low and the 24-hour high. 1. If the current_price was below the trigger_price, BTC was purchased. 1. If BTC was bought, no further buys would take place that day. 1. If the condition current_price < trigger_price: was not met any time up until 23:59 UTC, a buy would occur at 23:59 UTC. 1. The amount spent on BTC was the same each day. irrespective of whether the buy occurred at 23:59 or before then.

It was believed this strategy may work, giving an improvement over DCA, but clearly this needed to be tested properly. Due to lack of time, this was not initially tested, and just allowed to run.

Statistical analysis of historical bitcoin price.

A historical data set of BTC price on the Bitstamp exchange is available from Github reporisotry. This covers the period 1st January 2002 to the present day. One file contains data up to the end of 2015, and a second file, undated daily, contains more recent data.

library(data.table)
csv_file_BTC_2012_to_2025 <- "C:/Users/pc2/Documents/btc2/data/btcusd_bitstamp_1min_2012-2025.csv"
BTC_2012_to_2025 <- fread(csv_file_BTC_2012_to_2025)
head(BTC_2012_to_2025)
    timestamp  open  high   low close volume
        <int> <num> <num> <num> <num>  <num>
1: 1325412060  4.58  4.58  4.58  4.58      0
2: 1325412120  4.58  4.58  4.58  4.58      0
3: 1325412180  4.58  4.58  4.58  4.58      0
4: 1325412240  4.58  4.58  4.58  4.58      0
5: 1325412300  4.58  4.58  4.58  4.58      0
6: 1325412360  4.58  4.58  4.58  4.58      0

The first column is ihe time in seconds since the UNIX Epoch (1/1/1970). It was decided best to ignore data before the price reached exceeded $10,000, as this would not be represitive of how trading of bitcoin occurs today.

BTC_test <- BTC_2012_to_2025[
  low > 10000 &
  volume > 0
]
first_timestamp <- BTC_test$timestamp[1]

first_datetime <- as.POSIXct(
  first_timestamp,
  origin = "1970-01-01",
  tz = "UTC"
)
first_datetime
[1] "2017-11-29 07:40:00 UTC"
BTC_test[1]
    timestamp  open     high   low    close   volume
        <int> <num>    <num> <num>    <num>    <num>
1: 1511941200 10010 10035.39 10010 10035.39 76.33833

This occurred first on 2017-11-29. It was decided to start using data from the 1st January 2018, which would be

start_time <- as.numeric(
  as.POSIXct("2018-01-01 00:00:00", tz = "UTC")
)

BTC_since_2018 <- BTC_2012_to_2025[
  timestamp >= start_time
]
rm(BTC_2012_to_2025,BTC_test)
head(BTC_since_2018)
    timestamp     open     high      low    close     volume
        <int>    <num>    <num>    <num>    <num>      <num>
1: 1514764800 13840.53 13840.53 13819.04 13820.96  1.0455222
2: 1514764860 13821.05 13888.90 13821.05 13850.67 18.0838674
3: 1514764920 13888.91 13938.63 13850.01 13850.40  3.5536849
4: 1514764980 13850.40 13933.94 13850.40 13894.25  0.8943568
5: 1514765040 13894.26 13978.24 13879.83 13880.45 11.2142077
6: 1514765100 13880.45 13900.01 13880.00 13885.39  0.5678873

Next, the amount of BTC collected was determined if bought at 00:00, 12:00 or 23:59, and compared to what was obtained if used at various trigger levels.

library(data.table)

setDT(BTC_since_2018)

buy_usd <- 100
# start_date <- as.Date("2018-01-02")
start_date <- as.Date("2022-01-02")

BTC_since_2018[, datetime := as.POSIXct(
  timestamp,
  origin = "1970-01-01",
  tz = "UTC"
)]

BTC_since_2018[, date := as.Date(datetime, tz = "UTC")]
BTC_since_2018[, hhmm := format(datetime, "%H:%M")]
BTC_since_2018[, hour := as.integer(format(datetime, "%H"))]

# Rolling 24-hour high/low, including the current minute,
# to approximate Binance's reported previous 24-hour high/low.
BTC_since_2018[, high_24h := frollmax(high, n = 1440, align = "right")]
BTC_since_2018[, low_24h  := frollmin(low,  n = 1440, align = "right")]

# Fixed-time DCA comparisons, using opening price of that minute
#fixed_times <- c("00:00", "06:00", "12:00", "18:00", "23:59")
fixed_times <- sprintf("%02d:00",0:23)

fixed_time_results <- rbindlist(lapply(fixed_times, function(t) {
  buys <- BTC_since_2018[
    date >= start_date &
      hhmm == t
  ]

  btc_bought <- buy_usd / buys$open

  data.table(
    strategy = t,
    days_bought = nrow(buys),
    total_usd_spent = nrow(buys) * buy_usd,
    total_btc_bought = sum(btc_bought),
    average_buy_price = (nrow(buys) * buy_usd) / sum(btc_bought)
  )
}))

test_k <- function(k) {

  BTC_since_2018[, trigger_price := low_24h + k * (high_24h - low_24h)]

  trigger_buys <- BTC_since_2018[
    date >= start_date &
      !is.na(trigger_price) &
      open < trigger_price,
    .SD[1],
    by = date
  ]

  trigger_buys[, buy_type := "trigger"]
  trigger_buys[, buy_price := open]

  fallback_buys <- BTC_since_2018[
    date >= start_date &
      hhmm == "23:59" &
      !(date %in% trigger_buys$date)
  ]

  fallback_buys[, buy_type := "23:59 fallback"]
  fallback_buys[, buy_price := open]

  all_buys <- rbindlist(
    list(trigger_buys, fallback_buys),
    use.names = TRUE,
    fill = TRUE
  )

  all_buys[, btc_bought := buy_usd / buy_price]

  data.table(
    k = k,
    days_bought = nrow(all_buys),
    trigger_buys = nrow(trigger_buys),
    fallback_buys = nrow(fallback_buys),
    percent_trigger = 100 * nrow(trigger_buys) / nrow(all_buys),
    total_usd_spent = nrow(all_buys) * buy_usd,
    total_btc_bought = sum(all_buys$btc_bought),
    average_buy_price = (nrow(all_buys) * buy_usd) / sum(all_buys$btc_bought),
    mean_buy_hour = mean(all_buys$hour),
    median_buy_hour = median(all_buys$hour)
  )
}

k_values <- seq(0.01, 0.99, by = 0.01)

k_results <- rbindlist(lapply(k_values, test_k))

fixed_time_results[order(-total_btc_bought)]
    strategy days_bought total_usd_spent total_btc_bought average_buy_price
      <char>       <int>           <num>            <num>             <num>
 1:    00:00        1102          110200         3.371619          32684.60
 2:    01:00        1101          110100         3.370034          32670.29
 3:    11:00        1101          110100         3.369985          32670.77
 4:    08:00        1101          110100         3.369767          32672.88
 5:    09:00        1101          110100         3.369646          32674.05
 6:    22:00        1101          110100         3.369580          32674.70
 7:    07:00        1101          110100         3.369556          32674.93
 8:    12:00        1101          110100         3.369499          32675.49
 9:    10:00        1101          110100         3.369373          32676.70
10:    18:00        1101          110100         3.369305          32677.36
11:    04:00        1101          110100         3.369177          32678.61
12:    02:00        1101          110100         3.369176          32678.61
13:    14:00        1101          110100         3.369121          32679.14
14:    13:00        1101          110100         3.369090          32679.44
15:    21:00        1101          110100         3.368998          32680.34
16:    23:00        1101          110100         3.368964          32680.67
17:    03:00        1101          110100         3.368847          32681.81
18:    05:00        1101          110100         3.368655          32683.67
19:    17:00        1101          110100         3.368636          32683.85
20:    20:00        1101          110100         3.368631          32683.90
21:    06:00        1101          110100         3.368623          32683.98
22:    19:00        1101          110100         3.368525          32684.92
23:    15:00        1101          110100         3.368425          32685.89
24:    16:00        1101          110100         3.368419          32685.96
    strategy days_bought total_usd_spent total_btc_bought average_buy_price
      <char>       <int>           <num>            <num>             <num>
k_results[order(-total_btc_bought)][1:20]
        k days_bought trigger_buys fallback_buys percent_trigger
    <num>       <int>        <int>         <int>           <num>
 1:  0.95        1102         1102             0       100.00000
 2:  0.96        1102         1102             0       100.00000
 3:  0.97        1102         1102             0       100.00000
 4:  0.98        1102         1102             0       100.00000
 5:  0.99        1102         1102             0       100.00000
 6:  0.01        1101          422           679        38.32879
 7:  0.91        1101         1101             0       100.00000
 8:  0.92        1101         1101             0       100.00000
 9:  0.94        1101         1101             0       100.00000
10:  0.84        1101         1101             0       100.00000
11:  0.93        1101         1101             0       100.00000
12:  0.89        1101         1101             0       100.00000
13:  0.85        1101         1101             0       100.00000
14:  0.83        1101         1101             0       100.00000
15:  0.69        1101         1096             5        99.54587
16:  0.86        1101         1101             0       100.00000
17:  0.87        1101         1101             0       100.00000
18:  0.02        1101          513           588        46.59401
19:  0.88        1101         1101             0       100.00000
20:  0.90        1101         1101             0       100.00000
        k days_bought trigger_buys fallback_buys percent_trigger
    <num>       <int>        <int>         <int>           <num>
    total_usd_spent total_btc_bought average_buy_price mean_buy_hour
              <num>            <num>             <num>         <num>
 1:          110200         3.371734          32683.48  0.000000e+00
 2:          110200         3.371705          32683.76  0.000000e+00
 3:          110200         3.371679          32684.01  0.000000e+00
 4:          110200         3.371652          32684.27  0.000000e+00
 5:          110200         3.371645          32684.34  0.000000e+00
 6:          110100         3.371594          32655.18  1.820163e+01
 7:          110100         3.370774          32663.12  7.266122e-03
 8:          110100         3.370760          32663.25  4.541326e-03
 9:          110100         3.370724          32663.60  9.082652e-04
10:          110100         3.370691          32663.93  1.671208e-01
11:          110100         3.370688          32663.96  1.816530e-03
12:          110100         3.370669          32664.14  2.452316e-02
13:          110100         3.370658          32664.25  1.335150e-01
14:          110100         3.370655          32664.28  2.052679e-01
15:          110100         3.370629          32664.53  1.518619e+00
16:          110100         3.370626          32664.56  8.900999e-02
17:          110100         3.370608          32664.73  6.630336e-02
18:          110100         3.370599          32664.82  1.700636e+01
19:          110100         3.370596          32664.84  4.722979e-02
20:          110100         3.370590          32664.90  1.725704e-02
    total_usd_spent total_btc_bought average_buy_price mean_buy_hour
              <num>            <num>             <num>         <num>
    median_buy_hour
              <num>
 1:               0
 2:               0
 3:               0
 4:               0
 5:               0
 6:              23
 7:               0
 8:               0
 9:               0
10:               0
11:               0
12:               0
13:               0
14:               0
15:               0
16:               0
17:               0
18:              23
19:               0
20:               0
    median_buy_hour
              <num>
best_k <- k_results[which.max(total_btc_bought), k]
best_k
[1] 0.95
plot(
  k_results$k,
  k_results$total_btc_bought,
  type = "b",
  xlab = "k",
  ylab = "Total BTC bought",
  main = "BTC accumulated versus trigger parameter k"
)

abline(
  h = max(fixed_time_results$total_btc_bought),
  lty = 2
)

legend(
  "bottomright",
  legend = c("Trigger strategy", "Best fixed-time strategy"),
  lty = c(1, 2),
  pch = c(1, NA)
)

plot(
  k_results$k,
  k_results$average_buy_price,
  type = "b",
  xlab = "k",
  ylab = "Average buy price / USD",
  main = "Average buy price versus trigger parameter k"
)

# Recreate the actual buy list for the best k
BTC_since_2018[, trigger_price := low_24h + best_k * (high_24h - low_24h)]

best_trigger_buys <- BTC_since_2018[
  date >= start_date &
    !is.na(trigger_price) &
    open < trigger_price,
  .SD[1],
  by = date
]

best_trigger_buys[, buy_type := "trigger"]
best_trigger_buys[, buy_price := open]

best_fallback_buys <- BTC_since_2018[
  date >= start_date &
    hhmm == "23:59" &
    !(date %in% best_trigger_buys$date)
]

best_fallback_buys[, buy_type := "23:59 fallback"]
best_fallback_buys[, buy_price := open]

best_buys <- rbindlist(
  list(best_trigger_buys, best_fallback_buys),
  use.names = TRUE,
  fill = TRUE
)

best_buys[, btc_bought := buy_usd / buy_price]

buys_by_hour <- best_buys[
  ,
  .N,
  by = hour
][order(hour)]

buys_by_hour
    hour     N
   <int> <int>
1:     0  1102
barplot(
  buys_by_hour$N,
  names.arg = buys_by_hour$hour,
  xlab = "Hour of day UTC",
  ylab = "Number of buys",
  main = paste("Buy times for best k =", best_k)
)

hourly_results <- rbindlist(lapply(0:23, function(h) {
  
  t <- sprintf("%02d:00", h)
  
  buys <- BTC_since_2018[
    date >= start_date &
      hhmm == t
  ]
  
  btc_bought <- buy_usd / buys$open
  
  data.table(
    hour = h,
    time = t,
    days_bought = nrow(buys),
    total_usd_spent = nrow(buys) * buy_usd,
    total_btc_bought = sum(btc_bought),
    average_buy_price = (nrow(buys) * buy_usd) / sum(btc_bought)
  )
}))

hourly_results[order(-total_btc_bought)]
     hour   time days_bought total_usd_spent total_btc_bought average_buy_price
    <int> <char>       <int>           <num>            <num>             <num>
 1:     0  00:00        1102          110200         3.371619          32684.60
 2:     1  01:00        1101          110100         3.370034          32670.29
 3:    11  11:00        1101          110100         3.369985          32670.77
 4:     8  08:00        1101          110100         3.369767          32672.88
 5:     9  09:00        1101          110100         3.369646          32674.05
 6:    22  22:00        1101          110100         3.369580          32674.70
 7:     7  07:00        1101          110100         3.369556          32674.93
 8:    12  12:00        1101          110100         3.369499          32675.49
 9:    10  10:00        1101          110100         3.369373          32676.70
10:    18  18:00        1101          110100         3.369305          32677.36
11:     4  04:00        1101          110100         3.369177          32678.61
12:     2  02:00        1101          110100         3.369176          32678.61
13:    14  14:00        1101          110100         3.369121          32679.14
14:    13  13:00        1101          110100         3.369090          32679.44
15:    21  21:00        1101          110100         3.368998          32680.34
16:    23  23:00        1101          110100         3.368964          32680.67
17:     3  03:00        1101          110100         3.368847          32681.81
18:     5  05:00        1101          110100         3.368655          32683.67
19:    17  17:00        1101          110100         3.368636          32683.85
20:    20  20:00        1101          110100         3.368631          32683.90
21:     6  06:00        1101          110100         3.368623          32683.98
22:    19  19:00        1101          110100         3.368525          32684.92
23:    15  15:00        1101          110100         3.368425          32685.89
24:    16  16:00        1101          110100         3.368419          32685.96
     hour   time days_bought total_usd_spent total_btc_bought average_buy_price
    <int> <char>       <int>           <num>            <num>             <num>
# barchart 
btc_difference <- hourly_results$total_btc_bought -
  mean(hourly_results$total_btc_bought)

barplot(
  btc_difference,
  names.arg = sprintf("%02d", hourly_results$hour),
  xlab = "Hour of day UTC",
  ylab = "BTC difference from average hour",
  main = "Fixed purchase hour compared with average"
)

abline(h = 0, lty = 2)