Skip to content

Commit 4837942

Browse files
authored
finished chapter 18 (#20)
* finished chapter 18 * adding a moduledoc
1 parent 7fb100a commit 4837942

5 files changed

Lines changed: 329 additions & 215 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ erl_crash.dump
1111
postgres-data
1212
/tmp/
1313
.DS_Store
14+
*.swp

apps/naive/lib/naive/strategy.ex

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
defmodule Naive.Strategy do
2+
@moduledoc """
3+
Module in charge of making and executing descicions
4+
"""
5+
alias Core.Struct.TradeEvent
6+
alias Naive.Trader.State
7+
8+
require Logger
9+
10+
@binance_client Application.compile_env(:naive, :binance_client)
11+
@leader Application.compile_env(:naive, :leader)
12+
@logger Application.compile_env(:core, :logger)
13+
@pubsub_client Application.compile_env(:core, :pubsub_client)
14+
15+
def execute(%TradeEvent{} = trade_event, %State{} = state) do
16+
generate_decision(trade_event, state)
17+
|> execute_decision(state)
18+
end
19+
20+
def generate_decision(
21+
%TradeEvent{price: price},
22+
%State{
23+
budget: budget,
24+
buy_order: nil,
25+
buy_down_interval: buy_down_interval,
26+
tick_size: tick_size,
27+
step_size: step_size
28+
}
29+
) do
30+
price = calculate_buy_price(price, buy_down_interval, tick_size)
31+
quantity = calculate_quantity(budget, price, step_size)
32+
33+
{:place_buy_order, price, quantity}
34+
end
35+
36+
def generate_decision(
37+
%TradeEvent{buyer_order_id: order_id},
38+
%State{
39+
buy_order: %Binance.OrderResponse{
40+
order_id: order_id,
41+
status: "FILLED"
42+
},
43+
sell_order: %Binance.OrderResponse{}
44+
}
45+
)
46+
when is_number(order_id) do
47+
:skip
48+
end
49+
50+
def generate_decision(
51+
%TradeEvent{buyer_order_id: order_id},
52+
%State{
53+
buy_order: %Binance.OrderResponse{
54+
order_id: order_id
55+
},
56+
sell_order: nil
57+
}
58+
)
59+
when is_number(order_id) do
60+
:fetch_buy_order
61+
end
62+
63+
def generate_decision(
64+
%TradeEvent{},
65+
%State{
66+
buy_order: %Binance.OrderResponse{
67+
status: "FILLED",
68+
price: buy_price
69+
},
70+
sell_order: nil,
71+
profit_interval: profit_interval,
72+
tick_size: tick_size
73+
}
74+
) do
75+
sell_price = calculate_sell_price(buy_price, profit_interval, tick_size)
76+
77+
{:place_sell_order, sell_price}
78+
end
79+
80+
def generate_decision(
81+
%TradeEvent{},
82+
%State{
83+
sell_order: %Binance.OrderResponse{status: "FILLED"}
84+
}
85+
) do
86+
:exit
87+
end
88+
89+
def generate_decision(
90+
%TradeEvent{
91+
seller_order_id: order_id
92+
},
93+
%State{
94+
sell_order: %Binance.OrderResponse{
95+
order_id: order_id
96+
}
97+
}
98+
) do
99+
:fetch_sell_order
100+
end
101+
102+
def generate_decision(
103+
%TradeEvent{price: current_price},
104+
%State{
105+
buy_order: %Binance.OrderResponse{price: buy_price},
106+
rebuy_interval: rebuy_interval,
107+
rebuy_notified: false
108+
}
109+
) do
110+
if trigger_rebuy?(buy_price, current_price, rebuy_interval) do
111+
:rebuy
112+
else
113+
:skip
114+
end
115+
end
116+
117+
def generate_decision(%TradeEvent{}, _state) do
118+
:skip
119+
end
120+
121+
def calculate_sell_price(buy_price, profit_interval, tick_size) do
122+
fee = "1.001"
123+
original_price = Decimal.mult(buy_price, fee)
124+
125+
net_target_price = Decimal.mult(original_price, Decimal.add("1.0", profit_interval))
126+
127+
gross_target_price = Decimal.mult(net_target_price, fee)
128+
129+
Decimal.to_string(
130+
Decimal.mult(
131+
Decimal.div_int(gross_target_price, tick_size),
132+
tick_size
133+
),
134+
:normal
135+
)
136+
end
137+
138+
def calculate_buy_price(current_price, buy_down_interval, tick_size) do
139+
exact_buy_price = Decimal.sub(current_price, Decimal.mult(current_price, buy_down_interval))
140+
141+
Decimal.to_string(
142+
Decimal.mult(Decimal.div_int(exact_buy_price, tick_size), tick_size),
143+
:normal
144+
)
145+
end
146+
147+
def calculate_quantity(budget, price, step_size) do
148+
exact_target_quantity = Decimal.div(budget, price)
149+
150+
Decimal.to_string(
151+
Decimal.mult(Decimal.div_int(exact_target_quantity, step_size), step_size),
152+
:normal
153+
)
154+
end
155+
156+
def trigger_rebuy?(buy_price, current_price, rebuy_interval) do
157+
rebuy_price =
158+
Decimal.sub(buy_price, Decimal.mult(buy_price, rebuy_interval))
159+
160+
Decimal.lt?(current_price, rebuy_price)
161+
end
162+
163+
defp execute_decision(
164+
{:place_buy_order, price, quantity},
165+
%State{
166+
id: id,
167+
symbol: symbol
168+
} = state
169+
) do
170+
@logger.info(
171+
"The trader(#{id}) is placing a BUY order " <>
172+
"for #{symbol} @ #{price}, quantity: #{quantity}"
173+
)
174+
175+
{:ok, %Binance.OrderResponse{} = order} =
176+
@binance_client.order_limit_buy(symbol, quantity, price, "GTC")
177+
178+
:ok = broadcast_order(order)
179+
180+
new_state = %{state | buy_order: order}
181+
@leader.notify(:trader_state_updated, new_state)
182+
183+
{:ok, new_state}
184+
end
185+
186+
defp execute_decision(
187+
{:place_sell_order, sell_price},
188+
%State{
189+
id: id,
190+
symbol: symbol,
191+
buy_order: %Binance.OrderResponse{
192+
orig_qty: quantity
193+
}
194+
} = state
195+
) do
196+
@logger.info(
197+
"The trader(#{id}) is placing a SELL order for " <>
198+
"#{symbol} @ #{sell_price}, quantity: #{quantity}."
199+
)
200+
201+
{:ok, %Binance.OrderResponse{} = order} =
202+
@binance_client.order_limit_sell(symbol, quantity, sell_price, "GTC")
203+
204+
:ok = broadcast_order(order)
205+
206+
new_state = %{state | sell_order: order}
207+
208+
@leader.notify(:trader_state_updated, new_state)
209+
210+
{:ok, new_state}
211+
end
212+
213+
defp execute_decision(
214+
:fetch_buy_order,
215+
%State{
216+
id: id,
217+
symbol: symbol,
218+
buy_order:
219+
%Binance.OrderResponse{
220+
order_id: order_id,
221+
transact_time: timestamp
222+
} = buy_order
223+
} = state
224+
) do
225+
@logger.info("Trader's(#{id} #{symbol} buy order got partially filled")
226+
227+
{:ok, %Binance.Order{} = current_buy_order} =
228+
@binance_client.get_order(symbol, timestamp, order_id)
229+
230+
:ok = broadcast_order(current_buy_order)
231+
232+
buy_order = %{buy_order | status: current_buy_order.status}
233+
new_state = %{state | buy_order: buy_order}
234+
235+
@leader.notify(:trader_state_updated, new_state)
236+
237+
{:ok, new_state}
238+
end
239+
240+
defp execute_decision(
241+
:exit,
242+
%State{
243+
id: id,
244+
symbol: symbol
245+
}
246+
) do
247+
@logger.info("Trader(#{id}) finished trade cycle for #{symbol}")
248+
:exit
249+
end
250+
251+
defp execute_decision(
252+
:fetch_sell_order,
253+
%State{
254+
id: id,
255+
symbol: symbol,
256+
sell_order:
257+
%Binance.OrderResponse{
258+
order_id: order_id,
259+
transact_time: timestamp
260+
} = sell_order
261+
} = state
262+
) do
263+
@logger.info("Trader's(#{id} #{symbol} SELL order got partially filled")
264+
265+
{:ok, %Binance.Order{} = current_sell_order} =
266+
@binance_client.get_order(symbol, timestamp, order_id)
267+
268+
:ok = broadcast_order(current_sell_order)
269+
270+
new_state = %{state | sell_order: sell_order}
271+
@leader.notify(:trader_state_updated, new_state)
272+
{:ok, new_state}
273+
end
274+
275+
defp execute_decision(
276+
:rebuy,
277+
%State{
278+
id: id,
279+
symbol: symbol
280+
} = state
281+
) do
282+
@logger.info("Rebuy triggered for #{symbol} by the trader(#{id})")
283+
new_state = %{state | rebuy_notified: true}
284+
@leader.notify(:rebuy_triggered, new_state)
285+
{:ok, new_state}
286+
end
287+
288+
defp execute_decision(:skip, state) do
289+
{:ok, state}
290+
end
291+
292+
defp broadcast_order(%Binance.OrderResponse{} = response) do
293+
response
294+
|> convert_to_order
295+
|> broadcast_order
296+
end
297+
298+
defp broadcast_order(%Binance.Order{} = order) do
299+
@pubsub_client.broadcast(Core.PubSub, "ORDERS:#{order.symbol}", order)
300+
end
301+
302+
defp convert_to_order(%Binance.OrderResponse{} = response) do
303+
data = response |> Map.from_struct()
304+
305+
Binance.Order
306+
|> struct(data)
307+
|> Map.merge(%{
308+
cummulative_quote_qty: "0.00000000",
309+
stop_price: "0.00000000",
310+
iceberg_qty: "0.00000000",
311+
is_working: true
312+
})
313+
end
314+
end

0 commit comments

Comments
 (0)