Scalping Strategy for Cryptocurrency

Apply a scalping strategy to Bitcoin data from EODHD APIs

Michael Whittle
Coinmonks
Published in
10 min readApr 15, 2024

--

Licensed Image from Adobe Stock

What is a scalping strategy?

The concept is not as complicated as it sounds. Imagine that you are at a market, and you spot opportunities to buy items at one price and quickly sell them at a slightly higher price to someone else. That’s basically what scalping in trading is about — it’s about making quick, small profits by taking advantage of tiny price movements in the market.

You may have spotted a problem with this already, and you would be right. Exchanges charge fees on transactions. The fees on high frequency trading can be high and if you don’t incorporate it into your strategy, you can make a loss even if the trade itself is successful. I’ll show you how to deal with this.

Now, when we talk about applying this to cryptocurrency trading, and Bitcoin in particular, imagine you’re doing this in a super fast-paced digital market. Cryptos like Bitcoin can have their prices change pretty swiftly even in a short amount of time, which is perfect for scalping. You buy some Bitcoin, and then, as soon as its price goes up just a little, you sell it off for a small profit. Then, you do it all over again, many times a day.

As you would imagine, you need to trade using a short interval for this to work properly. For a more typical scalping approach, traders often use short time frames such as 1-minute, 5-minute, or 15-minute intervals, since scalping seeks to profit from small market movements within a very short timeframe. The 1-minute timeframe is very popular among scalpers for its potential to capture numerous small price movements throughout a trading day. However, the choice of timeframe can vary based on a trader’s specific strategy, risk tolerance, and the market being traded. For my demonstration I’m going to use 1-minute data provided by EODHD APIs.

Pros of Scalping Bitcoin

  • Quick Profits: If you’re sharp and quick, you can rack up a decent amount of small wins throughout the day. The crypto trading bot I developed called PyCryptoBot is perfect for automating this. You could also use a websocket as well.
  • Excitement: It’s pretty thrilling! You’re always on the lookout for opportunities, making quick decisions.
  • Use of Leverage: Some traders use leverage to amplify their trading power, potentially increasing profits from small price movements, but this generally doesn’t apply to cryptocurrency exchanges. You can however do this on a platform like IG, but you need to have a professional account which has significant risks to be aware of.

Cons of Scalping Bitcoin

  • High Risk: With the potential for reward comes high risk, especially if using leverage, as losses can also be amplified.
  • Fees Can Add Up: Every time you make a trade, there are fees. If you’re not careful, these can eat into your profits or even turn a winning strategy into a losing one. The fees need to be incorporated into the strategy logic.
  • Stressful and Time-Consuming: It’s not exactly laid back. You need to be constantly on the ball, making it a pretty intense way to trade.

Talking about fees, they’re super important to factor into your calculations. Every time you trade, the exchange takes a little cut. So, if you’re scalping, you’re making a lot of trades, which means lots of little fees. It’s a bit like death by a thousand cuts if you’re not paying attention. The price needs to move enough not just to make a profit, but to cover these fees as well. Otherwise, you could end up running really fast just to stay in place, or worse, losing money. Yes, I’m harping on about fees, but I can’t stress the importance enough.

So, in a nutshell, while scalping Bitcoin can be exciting and profitable due to its volatile nature, it’s not for the faint-hearted. You’ve got to be aware of the risks, particularly the impact of fees on your trading strategy. It’s a bit like playing a very fast-paced video game where real money is at stake. You’ve got to have a good strategy, be incredibly disciplined, and keep a cool head.

Strategy Overview

  1. Timeframe: 1 minute data.
  2. Buy Signal: When the fast SMA (5 periods) crosses above the slow SMA (12 periods), indicating upward momentum. We buy at the close price of the signal candle.
  3. Sell Signal: When the fast SMA (5 periods) crosses below the slow SMA (12 periods), indicating downward momentum. We sell at the close price of the signal candle.
  4. Exchange Fees: Assume a round-trip (buy and sell) exchange fee, which will be deducted from each trade. Let’s assume 0.6% taker fees from Coinbase Advanced Trade.
  5. Reinvesting: We reinvest all proceeds from each trade, including any remaining after accounting for the fee.
  6. Execution: The strategy will execute at the closing price following a signal.
  7. Initial Investment: The backtest will start with a defined initial investment amount.

Retrieving data to work with…

I’m going to retrieve 1-minute BTC-USD data from EODHD APIs.

import pandas as pd
from eodhd import APIClient
import config as cfg

api = APIClient(cfg.API_KEY)


def get_ohlc_data():
df = api.get_historical_data("BTC-USD.CC", "1m", results=120)
return df


if __name__ == "__main__":
df = get_ohlc_data()

df["close"] = pd.to_numeric(df["close"], errors="coerce")
df.dropna(subset=["close"], inplace=True)
df["close"].fillna(value=df["close"].mean(), inplace=True)

print(df)
Screenshot by Author

Applying a basic strategy

This is an example of a basic scalping strategy. Fine tuning it will definitely be recommended. I’ll show you how to backtest this next, so you will be able to evaluate how your refinement effects the results.

fast_sma_period = 5
slow_sma_period = 12
df["fast_sma"] = df["close"].rolling(window=fast_sma_period).mean()
df["slow_sma"] = df["close"].rolling(window=slow_sma_period).mean()

df["signal"] = 0 # default
df.loc[df["fast_sma"] > df["slow_sma"], "signal"] = 1 # buy signal
df.loc[df["fast_sma"] < df["slow_sma"], "signal"] = -1 # sell signal

print(df)
Screenshot by Author

Backtesting the strategy

fee_percent = 0.006  # 0.6%

initial_investment = 1000.0
cash = initial_investment
position = 0 # bitcoin balance

for i in range(1, len(df)):
if df.iloc[i]["signal"] == 1 and df.iloc[i - 1]["signal"] == -1:
# buy, assuming all cash is used
buy_amount = cash * (1 - fee_percent) # deduct fee
position = buy_amount / df.iloc[i]["close"]
cash = 0

elif df.iloc[i]["signal"] == -1 and df.iloc[i - 1]["signal"] == 1:
# sell, converting position to cash
cash = position * df.iloc[i]["close"] * (1 - fee_percent) # deduct fee
position = 0

final_value = cash if position == 0 else position * df.iloc[-1]["close"]
net_result = final_value - initial_investment

print(f"Final Value: {final_value:.2f}")
print(f"Net Result: {net_result:.2f} ({net_result / initial_investment * 100:.2f}%)")
Screenshot by Author

This is showing that using 120 minutes of data the strategy would result in a loss. The starting investment of £1000 would reduce to £914.38 in 120 minutes. A net result of -£85.62 (-8.56%). This isn’t really unexpected. The fees from Coinbase are very high, and the strategy incorporates the fees.

If you remove fees from the equation, it gives a positive result.

# fee_percent = 0.006  # 0.6%
fee_percent = 0.0 # 0.0%
Screenshot by Author

0.08% in 2 hours.

Trailing Stop Loss and Take-Profit

A potential way to improve on this while maintaining the inclusion of fees is to implement a trailing stop loss and/or an take-profit mechanism. The take-profit is often 3x the stop loss.

I’ve been experimenting with this and I’ve got it returning a positive result, but I had to lower the fees from 0.6% to 0.1%. Anything higher than this, at least for 1 minute intervals, with something as volatile as Bitcoin doesn’t seem to be consistent.

I also swapped the SMA crossovers to EMA which are better when working with cryptocurrencies as they are more responsive.

My code looks like this:

fast_ma_period = 12
slow_ma_period = 26
# df["fast_ma"] = df["close"].rolling(window=fast_ma_period).mean()
# df["slow_ma"] = df["close"].rolling(window=slow_ma_period).mean()
df["fast_ma"] = df["close"].ewm(span=fast_ma_period, adjust=False).mean()
df["slow_ma"] = df["close"].ewm(span=slow_ma_period, adjust=False).mean()

df["signal"] = 0 # default
df.loc[df["fast_ma"] > df["slow_ma"], "signal"] = 1 # buy signal
df.loc[df["fast_ma"] < df["slow_ma"], "signal"] = -1 # sell signal

fee_percent = 0.001 # 0.1%

initial_investment = 1000.0
cash = initial_investment
position = 0
high_since_buy = 0
buy_price = 0

trailing_stop_loss = 0.002 # E.g. 0.002 × 100 = 0.2%
take_profit = trailing_stop_loss * 3 # 3x trailing stop loss
stop_loss = 0.02 # E.g. 0.02 × 100 = 2%

for i in range(1, len(df)):
current_price = df.iloc[i]["close"]
if position > 0:
high_since_buy = max(high_since_buy, current_price)
if current_price <= high_since_buy * (1 - trailing_stop_loss):
cash = position * current_price * (1 - fee_percent)
position = 0
high_since_buy = 0
print(f"Trailing stop loss activated at {current_price}, cash now {cash}")
elif current_price >= buy_price * (1 + take_profit):
cash = position * current_price * (1 - fee_percent)
position = 0
high_since_buy = 0
print(f"Take profit activated at {current_price}, cash now {cash}")
elif current_price <= buy_price * (1 - stop_loss):
cash = position * current_price * (1 - fee_percent)
position = 0
high_since_buy = 0
print(f"Stop loss activated at {current_price}, cash now {cash}")

if df.iloc[i]["signal"] == 1 and df.iloc[i - 1]["signal"] != 1:
if cash > 0:
buy_amount = cash * (1 - fee_percent)
position = buy_amount / current_price
cash = 0
high_since_buy = current_price
buy_price = current_price
print(f"Bought at {current_price}, position now {position}")
elif df.iloc[i]["signal"] == -1 and df.iloc[i - 1]["signal"] != -1:
if position > 0:
cash = position * current_price * (1 - fee_percent)
position = 0
high_since_buy = 0
print(f"Sold at {current_price}, cash now {cash}")

final_value = cash if position == 0 else position * df.iloc[-1]["close"]
net_result = final_value - initial_investment

print(f"Final Value: {final_value:.2f}")
print(f"Net Result: {net_result:.2f} ({net_result / initial_investment * 100:.2f}%)")

It results in this…

Bought at 64034.67, position now 0.015600923687121368
Sold at 63988.53, cash now 997.2818932076951
Bought at 64011.68, position now 0.015564106602333939
Take profit at 64431.99, cash now 1001.8235345995538
Bought at 64399.0, position now 0.015540951118261995
Trailing stop loss at 64606.62, cash now 1003.0442750127916
Final Value: 1003.04
Net Result: 3.04 (0.30%)

Automation: Bot or Websocket

Ideally you want to automate this for it to be effective. You are dealing with a lot of analysis and updates in a short time frame, and it would be almost impossible to do it manually without making a mistake.

I was looking into how this could be done with a websocket. EODHD APIs provide a cryptocurrency websocket API, and you can find the documentation for it here.

I thought I would provide you with some code to get you started…

import datetime
import websocket
import json
import time
import threading
import signal
import config as cfg

data = {}
last_printed_minute = None


def on_message(ws, message):
global last_printed_minute
message_json = json.loads(message)

if "t" in message_json:
date_time = datetime.datetime.utcfromtimestamp(message_json["t"] / 1000.0)
floored_datetime = date_time.replace(second=0, microsecond=0)
iso_format = floored_datetime.isoformat()

if iso_format not in data:
data[iso_format] = {"sum": float(message_json["p"]), "count": 1}
else:
data[iso_format]["sum"] += float(message_json["p"])
data[iso_format]["count"] += 1

if last_printed_minute != iso_format:
if last_printed_minute is not None:
avg_price = (
data[last_printed_minute]["sum"]
/ data[last_printed_minute]["count"]
)
print(f"{last_printed_minute}: Average price: {avg_price}")
last_printed_minute = iso_format


def on_error(ws, error):
print("Error: " + str(error))


def on_close(ws, close_status_code, close_msg):
print("WebSocket closed")


def on_open(ws):
def run(*args):
payload = {
"action": "subscribe",
"symbols": "BTC-USD",
}
ws.send(json.dumps(payload))

# keep alive
while True:
time.sleep(50)
ws.send(json.dumps({"action": "keep_alive"}))

thread = threading.Thread(target=run)
thread.start()


def on_signal(signal, frame):
print("Signal received, closing WebSocket...")
ws.close()


if __name__ == "__main__":
websocket.enableTrace(False)
ws = websocket.WebSocketApp(
f"wss://ws.eodhistoricaldata.com/ws/crypto?api_token={cfg.API_KEY}",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)

signal.signal(signal.SIGINT, on_signal) # handle Ctrl-C
signal.signal(signal.SIGTERM, on_signal) # handle kill commands

ws.run_forever()

This code will connect to the websocket and subscribe to the BTC-USD messages. It will then aggregate all the data within a minute and calculate the average price for the minute and associate it with the minute timestamp in ISO format. It also has a keep alive to keep the websocket open, and it also will gracefully stop the websocket if the script terminates.

You will wind up with an output that looks something like this…

2024-04-14T20:26:00: Average price: 64106.09294307696
2024-04-14T20:27:00: Average price: 64108.821365536765
2024-04-14T20:28:00: Average price: 64137.0758961011
2024-04-14T20:29:00: Average price: 64134.83291021914
2024-04-14T20:30:00: Average price: 64162.79143044936
2024-04-14T20:31:00: Average price: 64212.15334335508

In my scalping code above, I’m using the EMA12 and EMA26. This means you will need to run the websocket for 26 minutes to be able to calculate the EMA26, that is required to calculate the buy/sell signal.

From this point onwards it’s the same process. As the websocket returns the next minute you would just calculate the moving averages again and then determine what to do with the result.

Conclusion

A scalping strategy does have potential. You definitely would want to automate the process with a bot or websocket. Doing this manually would give you grey hair in no time. You also want to find an exchange that have very low fees or a different fee structure. Binance is pretty good for this, their fees are much lower.

I hope you found this article interesting and useful. If you would like to be kept informed, please don’t forget to follow me and sign up to my email notifications.

If you liked this article, I recommend checking out EODHD APIs on Medium. They have some interesting articles.

Michael Whittle

--

--

Michael Whittle
Coinmonks

Solution Architect — CCIE R&S #24223 | Full-Stack / Blockchain / Web3 Developer | Security Specialist | PyCryptoBot Creator