Skip to content

Commit

Permalink
can play demo mode with DemoTrader
Browse files Browse the repository at this point in the history
  • Loading branch information
msaltnet committed Mar 15, 2023
1 parent e08cd5b commit 698b724
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 6 deletions.
2 changes: 2 additions & 0 deletions smtm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .virtual_market import VirtualMarket
from .worker import Worker
from .simulator import Simulator
from .demo_trader import DemoTrader
from .upbit_trader import UpbitTrader
from .bithumb_trader import BithumbTrader
from .upbit_data_provider import UpbitDataProvider
Expand All @@ -40,6 +41,7 @@
"Simulator",
"UpbitTrader",
"BithumbTrader",
"DemoTrader",
"UpbitDataProvider",
"MassSimulator",
]
Expand Down
5 changes: 4 additions & 1 deletion smtm/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
python -m smtm --mode 1
python -m smtm --mode 1 --budget 500 --from_dash_to 201220.170000-201221 --term 1 --strategy 0 --currency BTC
python -m smtm --mode 2 --budget 50000 --term 60 --strategy 0 --currency ETH
python -m smtm --mode 2 --budget 50000 --term 60 --strategy 0 --currency ETH --demo 1
python -m smtm --mode 3
python -m smtm --mode 3 --demo 1
python -m smtm --mode 4 --config /data/sma0_simulation.json
python -m smtm --mode 5 --budget 50000 --title SMA_2H_week --strategy 1 --currency ETH --from_dash_to 210804.000000-210811.000000 --offset 120 --file generated_config.json
"""
Expand Down Expand Up @@ -60,6 +62,7 @@
parser.add_argument("--file", help="generated config file name", default=None)
parser.add_argument("--offset", help="mass simulation period offset", type=int, default=120)
parser.add_argument("--log", help="log file name", default=None)
parser.add_argument("--demo", help="use demo trader", type=int, default=0)
parser.add_argument(
"--mode",
help="0: interactive simulator, 1: single simulation, 2: real trading",
Expand Down Expand Up @@ -103,7 +106,7 @@
controller.main()
elif args.mode == 3:
tcb = TelegramController()
tcb.main()
tcb.main(demo=args.demo == 1)
elif args.mode == 4:
if args.config == "":
parser.print_help()
Expand Down
192 changes: 192 additions & 0 deletions smtm/demo_trader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""데모를 위해 계좌 정보가 없이 가상으로 거래 요청 및 계좌 조회 요청을 처리하는 클래스"""

from datetime import datetime
import requests
from .log_manager import LogManager
from .trader import Trader
from .worker import Worker


class DemoTrader(Trader):
"""
거래 요청 정보를 받아서 거래소에 요청하고 거래소에서 받은 결과를 제공해주는 클래스
id: 요청 정보 id "1607862457.560075"
type: 거래 유형 sell, buy, cancel
price: 거래 가격
amount: 거래 수량
"""

RESULT_CHECKING_INTERVAL = 5
ISO_DATEFORMAT = "%Y-%m-%dT%H:%M:%S"
AVAILABLE_CURRENCY = {
"BTC": ("KRW-BTC", "BTC"),
"ETH": ("KRW-ETH", "ETH"),
"DOGE": ("KRW-DOGE", "DOGE"),
"XRP": ("KRW-XRP", "XRP"),
}
NAME = "DemoTrader"

def __init__(self, budget=50000, currency="BTC", commission_ratio=0.0005, opt_mode=True):
if currency not in self.AVAILABLE_CURRENCY:
raise UserWarning(f"not supported currency: {currency}")

self.logger = LogManager.get_logger(__class__.__name__)
self.worker = Worker("DemoTrader-Worker")
self.worker.start()
self.SERVER_URL = "https://api.upbit.com"
self.is_opt_mode = opt_mode
self.asset = (0, 0) # avr_price, amount
self.balance = budget
self.commission_ratio = commission_ratio
currency_info = self.AVAILABLE_CURRENCY[currency]
self.market = currency_info[0]
self.market_currency = currency_info[1]

@staticmethod
def _create_success_result(request):
return {
"state": "done",
"request": request,
"type": request["type"],
"price": request["price"],
"amount": request["amount"],
"msg": "success",
"date_time": request["date_time"],
}

def send_request(self, request_list, callback):
"""거래 요청을 처리한다
request_list: 한 개 이상의 거래 요청 정보 리스트
[{
"id": 요청 정보 id "1607862457.560075"
"type": 거래 유형 sell, buy, cancel
"price": 거래 가격
"amount": 거래 수량
"date_time": 요청 데이터 생성 시간
}]
callback(result):
{
"request": 요청 정보
"type": 거래 유형 sell, buy, cancel
"price": 거래 가격
"amount": 거래 수량
"state": 거래 상태 requested, done
"msg": 거래 결과 메세지
"date_time": 거래 체결 시간
}
"""
for request in request_list:
self._execute_order({"request": request, "callback": callback})

def get_account_info(self):
"""계좌 정보를 요청한다
Returns:
{
balance: 계좌 현금 잔고
asset: 자산 목록, 마켓이름을 키값으로 갖고 (평균 매입 가격, 수량)을 갖는 딕셔너리
quote: 종목별 현재 가격 딕셔너리
date_time: 현재 시간
}
"""
trade_info = self.get_trade_tick()
result = {
"balance": self.balance,
"asset": {self.market_currency: self.asset},
"quote": {},
"date_time": datetime.now().strftime(self.ISO_DATEFORMAT),
}
result["quote"][self.market_currency] = float(trade_info[0]["trade_price"])
self.logger.debug(f"account info {result}")
return result

def cancel_request(self, request_id):
"""거래 요청을 취소한다, 모든 요청은 바로 처리되므로 사용되지 않는다
request_id: 취소하고자 하는 request의 id
"""

def cancel_all_requests(self):
"""모든 거래 요청을 취소한다, 모든 요청은 바로 처리되므로 사용되지 않는다
체결되지 않고 대기중인 모든 거래 요청을 취소한다
"""

def get_trade_tick(self):
"""최근 거래 정보 조회"""
querystring = {"market": self.market, "count": "1"}
return self._request_get(self.SERVER_URL + "/v1/trades/ticks", params=querystring)

def _execute_order(self, task):
request = task["request"]
if request["type"] == "cancel":
self.cancel_request(request["id"])
return

if request["price"] == 0:
self.logger.warning("[REJECT] market price is not supported now")
task["callback"]("error!")
return

is_buy = request["type"] == "buy"
if is_buy and float(request["price"]) * float(request["amount"]) > self.balance:
request_price = float(request["price"]) * float(request["amount"])
self.logger.warning(f"[REJECT] balance is too small! {request_price} > {self.balance}")
task["callback"]("error!")
return

if is_buy is False and float(request["amount"]) > self.asset[1]:
self.logger.warning(
f"[REJECT] invalid amount {float(request['amount'])} > {self.asset[1]}"
)
task["callback"]("error!")
return

result = self._create_success_result(request)
self._call_callback(task["callback"], result)
self.logger.debug(f"request executed {request['id']}")

def _call_callback(self, callback, result):
result_value = float(result["price"]) * float(result["amount"])
fee = result_value * self.commission_ratio

if result["state"] == "done" and result["type"] == "buy":
old_value = self.asset[0] * self.asset[1]
new_value = old_value + result_value
new_amount = self.asset[1] + float(result["amount"])
new_amount = round(new_amount, 6)
if new_amount == 0:
avr_price = 0
else:
avr_price = round(new_value / new_amount, 6)
self.asset = (avr_price, new_amount)
self.balance -= round(result_value + fee)
elif result["state"] == "done" and result["type"] == "sell":
old_avr_price = self.asset[0]
new_amount = self.asset[1] - float(result["amount"])
new_amount = round(new_amount, 6)
if new_amount == 0:
old_avr_price = 0
self.asset = (old_avr_price, new_amount)
self.balance += round(result_value - fee)

callback(result)

def _request_get(self, url, headers=None, params=None):
try:
if params is not None:
response = requests.get(url, params=params, headers=headers)
else:
response = requests.get(url, headers=headers)
response.raise_for_status()
result = response.json()
except ValueError as err:
self.logger.error(f"Invalid data from server: {err}")
return None
except requests.exceptions.HTTPError as msg:
self.logger.error(msg)
return None
except requests.exceptions.RequestException as msg:
self.logger.error(msg)
return None

return result
18 changes: 14 additions & 4 deletions smtm/telegram_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
StrategySma0,
StrategyRsi,
Operator,
DemoTrader,
)

load_dotenv()
Expand Down Expand Up @@ -74,6 +75,7 @@ def __init__(self):
self.command_list = []
self._create_command()
self.currency = None
self.is_demo = False

def _create_command(self):
"""명령어 정보를 생성한다"""
Expand Down Expand Up @@ -149,13 +151,15 @@ def _convert_keyboard_markup(setup_list):
markup = json.dumps(markup)
item["keyboard"] = parse.quote(markup)

def main(self):
def main(self, demo=False):
"""main 함수"""
print("##### smtm telegram controller is started #####")
self.is_demo = demo
if self.is_demo:
print("$$$ THIS IS DEMO MODE $$$")

signal.signal(signal.SIGINT, self._terminate)
signal.signal(signal.SIGTERM, self._terminate)

self._start_get_updates_loop()
while not self.terminating:
try:
Expand Down Expand Up @@ -269,14 +273,20 @@ def _on_start_step3(self, command):
if command.upper() in ["1. UPBIT", "1", "UPBIT"]:
if self.currency in self.UPBIT_CURRENCY:
self.data_provider = UpbitDataProvider(currency=self.currency)
self.trader = UpbitTrader(budget=self.budget, currency=self.currency)
if self.is_demo:
self.trader = DemoTrader(budget=self.budget, currency=self.currency)
else:
self.trader = UpbitTrader(budget=self.budget, currency=self.currency)
not_ok = False
else:
self._send_text_message("현재 지원하지 않는 코인입니다.")
elif command.upper() in ["2. BITHUMB", "2", "BITHUMB"]:
if self.currency in self.BITHUMB_CURRENCY:
self.data_provider = BithumbDataProvider(currency=self.currency)
self.trader = BithumbTrader(budget=self.budget, currency=self.currency)
if self.is_demo:
self.trader = DemoTrader(budget=self.budget, currency=self.currency)
else:
self.trader = BithumbTrader(budget=self.budget, currency=self.currency)
not_ok = False
else:
self._send_text_message("현재 지원하지 않는 코인입니다.")
Expand Down
7 changes: 6 additions & 1 deletion smtm/upbit_trader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ class UpbitTrader(Trader):

RESULT_CHECKING_INTERVAL = 5
ISO_DATEFORMAT = "%Y-%m-%dT%H:%M:%S"
AVAILABLE_CURRENCY = {"BTC": ("KRW-BTC", "BTC"), "ETH": ("KRW-ETH", "ETH"), "DOGE": ("KRW-DOGE", "DOGE"), "XRP": ("KRW-XRP", "XRP")}
AVAILABLE_CURRENCY = {
"BTC": ("KRW-BTC", "BTC"),
"ETH": ("KRW-ETH", "ETH"),
"DOGE": ("KRW-DOGE", "DOGE"),
"XRP": ("KRW-XRP", "XRP"),
}
NAME = "Upbit"

def __init__(self, budget=50000, currency="BTC", commission_ratio=0.0005, opt_mode=True):
Expand Down
Loading

0 comments on commit 698b724

Please sign in to comment.