Repository navigation
Expand file tree
/
Copy pathsimple_vwap.py
More file actions
215 lines (191 loc) · 10.5 KB
/
Copy pathsimple_vwap.py
File metadata and controls
215 lines (191 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import logging
import math
import os
from decimal import Decimal
from typing import Dict, List
from pydantic import Field
from hummingbot.connector.connector_base import ConnectorBase
from hummingbot.connector.utils import split_hb_trading_pair
from hummingbot.core.data_type.common import MarketDict
from hummingbot.core.data_type.order_candidate import OrderCandidate
from hummingbot.core.event.events import OrderFilledEvent, OrderType, TradeType
from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase
class VWAPConfig(StrategyV2ConfigBase):
"""
Configuration parameters for the VWAP strategy.
"""
script_file_name: str = os.path.basename(__file__)
controllers_config: List[str] = []
connector_name: str = Field("binance_paper_trade", json_schema_extra={
"prompt": lambda mi: "Exchange where the bot will place orders",
"prompt_on_new": True})
trading_pair: str = Field("ETH-USDT", json_schema_extra={
"prompt": lambda mi: "Trading pair where the bot will place orders",
"prompt_on_new": True})
is_buy: bool = Field(True, json_schema_extra={
"prompt": lambda mi: "Buying or selling the base asset? (True for buy, False for sell)",
"prompt_on_new": True})
total_volume_quote: Decimal = Field(1000, json_schema_extra={
"prompt": lambda mi: "Total volume to buy/sell (in quote asset)",
"prompt_on_new": True})
price_spread: float = Field(0.001, json_schema_extra={
"prompt": lambda mi: "Maximum price spread to use when placing orders (0.001 = 0.1%)",
"prompt_on_new": True})
volume_perc: float = Field(0.001, json_schema_extra={
"prompt": lambda mi: "Percentage of the order book volume to buy/sell (0.001 = 0.1%)",
"prompt_on_new": True})
order_delay_time: int = Field(10, json_schema_extra={
"prompt": lambda mi: "Delay time between orders (in seconds)",
"prompt_on_new": True})
def update_markets(self, markets: MarketDict) -> MarketDict:
markets[self.connector_name] = markets.get(self.connector_name, set()) | {self.trading_pair}
return markets
class VWAPExample(StrategyV2Base):
"""
BotCamp Cohort: 7 (Apr 2024)
Description:
This is an updated version of simple_vwap_example.py. Changes include:
- Users can define script configuration parameters
- Total volume is expressed in quote asset rather than USD
- Use of the rate oracle has been removed
"""
def __init__(self, connectors: Dict[str, ConnectorBase], config: VWAPConfig):
super().__init__(connectors, config)
self.config = config
self.initialized = False
self.vwap: Dict = {"connector_name": self.config.connector_name,
"trading_pair": self.config.trading_pair,
"is_buy": self.config.is_buy,
"total_volume_quote": self.config.total_volume_quote,
"price_spread": self.config.price_spread,
"volume_perc": self.config.volume_perc,
"order_delay_time": self.config.order_delay_time}
last_ordered_ts = 0
def on_tick(self):
"""
Every order delay time the strategy will buy or sell the base asset. It will compute the cumulative order book
volume until the spread and buy a percentage of that.
The input of the strategy is in quote, and we will convert at initial price to get a target base that will be static.
- Create proposal (a list of order candidates)
- Check the account balance and adjust the proposal accordingly (lower order amount if needed)
- Lastly, execute the proposal on the exchange
"""
if self.last_ordered_ts < (self.current_timestamp - self.vwap["order_delay_time"]):
if self.vwap.get("status") is None:
self.init_vwap_stats()
elif self.vwap.get("status") == "ACTIVE":
vwap_order: OrderCandidate = self.create_order()
vwap_order_adjusted = self.vwap["connector"].budget_checker.adjust_candidate(vwap_order,
all_or_none=False)
if math.isclose(vwap_order_adjusted.amount, Decimal("0"), rel_tol=1E-5):
self.logger().info(f"Order adjusted: {vwap_order_adjusted.amount}, too low to place an order")
else:
self.place_order(
connector_name=self.vwap["connector_name"],
trading_pair=self.vwap["trading_pair"],
is_buy=self.vwap["is_buy"],
amount=vwap_order_adjusted.amount,
order_type=vwap_order_adjusted.order_type,
price=vwap_order_adjusted.price)
self.last_ordered_ts = self.current_timestamp
def init_vwap_stats(self):
# General parameters
vwap = self.vwap.copy()
vwap["connector"] = self.connectors[vwap["connector_name"]]
vwap["delta"] = 0
vwap["trades"] = []
vwap["status"] = "ACTIVE"
vwap["trade_type"] = TradeType.BUY if self.vwap["is_buy"] else TradeType.SELL
vwap["start_price"] = vwap["connector"].get_price(vwap["trading_pair"], vwap["is_buy"])
vwap["target_base_volume"] = vwap["total_volume_quote"] / vwap["start_price"]
# Compute market order scenario
orderbook_query = vwap["connector"].get_quote_volume_for_base_amount(vwap["trading_pair"], vwap["is_buy"],
vwap["target_base_volume"])
vwap["market_order_base_volume"] = orderbook_query.query_volume
vwap["market_order_quote_volume"] = orderbook_query.result_volume
vwap["volume_remaining"] = vwap["target_base_volume"]
vwap["real_quote_volume"] = Decimal(0)
self.vwap = vwap
def create_order(self) -> OrderCandidate:
"""
Retrieves the cumulative volume of the order book until the price spread is reached, then takes a percentage
of that to use as order amount.
"""
# Compute the new price using the max spread allowed
mid_price = float(self.vwap["connector"].get_mid_price(self.vwap["trading_pair"]))
price_multiplier = 1 + self.vwap["price_spread"] if self.vwap["is_buy"] else 1 - self.vwap["price_spread"]
price_affected_by_spread = mid_price * price_multiplier
# Query the cumulative volume until the price affected by spread
orderbook_query = self.vwap["connector"].get_volume_for_price(
trading_pair=self.vwap["trading_pair"],
is_buy=self.vwap["is_buy"],
price=price_affected_by_spread)
volume_for_price = orderbook_query.result_volume
# Check if the volume available is higher than the remaining
amount = min(volume_for_price * Decimal(self.vwap["volume_perc"]), Decimal(self.vwap["volume_remaining"]))
# Quantize the order amount and price
amount = self.vwap["connector"].quantize_order_amount(self.vwap["trading_pair"], amount)
price = self.vwap["connector"].quantize_order_price(self.vwap["trading_pair"],
Decimal(price_affected_by_spread))
# Create the Order Candidate
vwap_order = OrderCandidate(
trading_pair=self.vwap["trading_pair"],
is_maker=False,
order_type=OrderType.MARKET,
order_side=self.vwap["trade_type"],
amount=amount,
price=price)
return vwap_order
def place_order(self,
connector_name: str,
trading_pair: str,
is_buy: bool,
amount: Decimal,
order_type: OrderType,
price=Decimal("NaN"),
):
if is_buy:
self.buy(connector_name, trading_pair, amount, order_type, price)
else:
self.sell(connector_name, trading_pair, amount, order_type, price)
def did_fill_order(self, event: OrderFilledEvent):
"""
Listens to fill order event to log it and notify the Hummingbot application.
"""
if event.trading_pair == self.vwap["trading_pair"] and event.trade_type == self.vwap["trade_type"]:
self.vwap["volume_remaining"] -= event.amount
self.vwap["delta"] = (self.vwap["target_base_volume"] - self.vwap["volume_remaining"]) / self.vwap[
"target_base_volume"]
self.vwap["real_quote_volume"] += event.price * event.amount
self.vwap["trades"].append(event)
if math.isclose(self.vwap["delta"], 1, rel_tol=1e-5):
self.vwap["status"] = "COMPLETE"
msg = (f"({event.trading_pair}) {event.trade_type.name} order (price: {round(event.price, 2)}) of "
f"{round(event.amount, 2)} "
f"{split_hb_trading_pair(event.trading_pair)[0]} is filled.")
self.log_with_clock(logging.INFO, msg)
self.notify_hb_app_with_timestamp(msg)
def format_status(self) -> str:
"""
Returns status of the current strategy on user balances and current active orders. This function is called
when status command is issued. Override this function to create custom status display output.
"""
if not self.ready_to_trade:
return "Market connectors are not ready."
lines = []
warning_lines = []
warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples()))
balance_df = self.get_balance_df()
lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
try:
df = self.active_orders_df()
lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")])
except ValueError:
lines.extend(["", " No active maker orders."])
lines.extend(["", "VWAP Info:"] + [" " + key + ": " + value
for key, value in self.vwap.items()
if isinstance(value, str)])
lines.extend(["", "VWAP Stats:"] + [" " + key + ": " + str(round(value, 4))
for key, value in self.vwap.items()
if type(value) in [int, float, Decimal]])
return "\n".join(lines)