첫 번째 커밋

This commit is contained in:
javamon
2025-12-06 22:31:19 +09:00
commit 849a100fa9
33 changed files with 6613 additions and 0 deletions

521
trader.py Normal file
View File

@@ -0,0 +1,521 @@
# -*- coding: utf-8 -*-
# 데이터베이스(MYSQL) ----
from db import DB
# Email
from mail import Mail
import importlib
# 지표 적용 모듈 로드
from indicator_util import *
import json
import time
import sys, os
# trade 테이블(거래 내역) : 금융, 거래소, 매매 타입, 종목, 시간타입, 가격, 목표가, 매매전략, 알림유무, 일자
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + '/collectors'
'''
- (매시간 단위)Base 테이블의 새 데이터 추가시 테이블 생성(종목-거래소)
- (매일 단위) 거래소-종목 거래량 미달 시 해당 종목 포지션 청산 후 테이블 삭제
- (매분 단위)종목-거래소별 Tinker 정보 저장(1분, 1시간, 1일) => 1분만 할지 다시 고민
- (매시간 단위) 시간봉 데이터 저장 및 분석 클래스에 전달
- (매일 단위) 일봉 데이터 저장 및 분석 클래스에 전달
(현행)
- 트레이더 클래스 구현
- 트레이더 유저별 거래 기능 구현 => 진행중
- 거래소별 반복문 => 시뮬레이션 완료된 시간 종목만
- 차트 데이터 로드(거래소 or DB)
- 매매 전략 적용(현재 포지션 상태 가져오기)
- 매매 구현 => 거래소별 => 트레이드 테이블에 데이터 추가
- 스톱 로스/프로핏 구현
- 분할 매매 => 보류(구현만)
- 파동 관점 타켓가 구현 => 보류(구현만)
- 트레이딩 데이터 DB 저장 구현
- 봇 데이터 생성 및 상태 지속적으로 유지 구현
- 시뮬레이터 최고 수익/승률 시뮬레이팅 부분 재확인 및 보완
'''
class Trader:
db = DB()
mail = Mail()
trade_list = None
api_obj = {}
def send_email(self, title, msg):
self.mail.send_email(title, msg)
def run(self):
print('Oh! my bot. Trading Bot Working.')
users = self.get_users_id()
# 유저 수 비례 반복문
for user in users:
bot_list = self.bot_list_by_user_id(user['id'])
# 작동중인 봇 리스트 반복문(상태값 Y)
for b in bot_list:
self.bot_trading(b)
def bot_trading(self, bot_info):
# bot_info['time_unit'] = 'hour6' # for test
# bot_info['target'] = 'crypto_bithumb_KRW-BTC'
d = self._get_price_data_by_info(bot_info)
current_price = float(self._get_last_price(bot_info))
# 포지션 존재 시
if bot_info['position'] != 'None' or str(bot_info['position']).lower() != 'none':
bot_info['position_price'] = float(bot_info['position_price'])
# 스탑 로스 / 프로핏 선실행
if bot_info['stop_profit'] > 0: # long - profit / short - loss
tp = bot_info['position_price'] + float(bot_info['stop_profit'])*bot_info['position_price']
if tp < current_price:
bot_info['last_signal'] = d.index[-1]
return self.close_position(bot_info)
if bot_info['stop_loss'] > 0: # long - loss / short - profit
lp = bot_info['position_price'] - float(bot_info['stop_loss'])*bot_info['position_price']
if lp > current_price:
bot_info['last_signal'] = d.index[-1]
return self.close_position(bot_info)
is_stop = False
# 손절 로직 => 5퍼 이상 손실일 경우 강제 손절 => 종목 시뮬레이팅 재실행
stop_persent = 0.056
# 숏 포지션일 경우
if bot_info['position'].lower() == 'short':
limit = bot_info['position_price'] + bot_info['position_price'] * stop_persent
if current_price > limit:
is_stop = True
# 롱 포지션일 경우
if bot_info['position'].lower() == 'long':
limit = bot_info['position_price'] - bot_info['position_price'] * stop_persent
if current_price < limit:
is_stop = True
if is_stop:
self.db.update_bot_data_for_stop_by_id(bot_info['id'])
self._init_simul_status(bot_info['target'])
self.close_position(bot_info)
title = '[%s] - 봇이 손절프로세스를 실행합니다.' % (bot_info['target'])
body = "\n".join('{} : {}'.format(key, value) for key, value in bot_info.items())
return self.send_email(title, body)
# 최근 시그널에 포지션 진입했을 경우 생략
if str(d.index[-1]) == str(bot_info['last_signal']):
print('Previous signal is equal')
return
# 시그널 획득
signal = self._get_trade_signal_by_data(d, bot_info)
# 포지션 진입 및 종료
if signal is not None and bot_info['position'] != signal:
bot_info['last_signal'] = d.index[-1]
if bot_info['trade_type'] == 'double' and bot_info['position'] != 'None':
self.close_position(bot_info) # for swap
if signal == 'long':
self.long(bot_info)
elif signal == 'short':
self.short(bot_info)
del d, signal
def _init_simul_status(self, target):
t = str(target).split('_')
i = '/'.join(t[:-1])
self.db.update_tb_cron_simul_status(i)
def _get_last_price(self, b_info):
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
return self.api_obj[e_name].get_current_price(i_name)
'''
Only Long - Single
- Long Signal : 매수 진입
- Short Signal : 매도(포지션 종료)
- Stop Signal(close_position) : 현재 포지션 종료
Margin(Swap) - Double
- Long Signal : 숏 포지션 종료 => 롱 포지션 진입
- Short Signal : 롱 포지션 종료 => 숏 포지션 진입
- Stop Signal(close_position) : 현재 포지션 종료
'''
def long(self, b_info, is_swap=False):
time.sleep(1)
if str(b_info['position']).lower() == 'long':
return
profit = None
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
# 수익 발생 시 봇 모드 변경
if b_info['mode'] == 'test':
price = self._get_last_order_price_by_position(b_info, 'long')
if b_info['position'] == 'short':
profit = bool(float(price) < float(b_info['position_price']))
b_info['position'] = 'long'
b_info['position_price'] = price
b_info['trade_cnt'] += 1
if profit:
# 봇 정보 초기화
b_info['mode'] = 'service'
b_info['position'] = 'None'
b_info['position_price'] = 0
b_info['profit'] = 0
b_info['winrate'] = 0
b_info['trade_cnt'] = 0
b_info['win_cnt'] = 0
b_info['lose_cnt'] = 0
# 계정 자산(원화) 동기화 => 첫 포지션 진입 시로 대체
# amt = self._get_amount_from_exchange(b_info)
b_info['amount'] = 0
b_info['last_amount'] = 0
title = '[%s] %s - 모의트레이딩 종료 알림' % (e_name, i_name)
body = "\n".join('{} : {}'.format(key, value) for key, value in b_info.items())
self.send_email(title, body)
if is_swap:
b_info['position'] = 'None'
b_info['position_price'] = 0
self.db.bot_update_by_trade_data(b_info)
return
res, info = self.api_obj[e_name].order_long(i_name)
if res:
profit = 0
info['seeds'] = self.api_obj[e_name].get_all_seeds()
if float(b_info['last_amount']) != 0:
b_info['last_profit'] = round((float(info['seeds']) / float(b_info['last_amount']) - 1) * 100, 2)
b_info['last_amount'] = info['seeds']
if b_info['amount'] != 0:
profit = round((float(info['seeds']) / float(b_info['amount']) - 1) * 100, 2)
else:
b_info['amount'] = info['seeds']
# 포지션 종료 및 수익 데이터 저장
if b_info['position'] != 'None':
b_info['trade_cnt'] += 1
b_info['profit'] = profit
# 수익률 기반 승률 산출
if profit > 0:
b_info['win_cnt'] += 1
b_info['winrate'] = round((int(b_info['win_cnt'])/(int(b_info['win_cnt'])+int(b_info['lose_cnt'])))*100, 2)
else:
b_info['lose_cnt'] += 1
# 수익 25프로 이상 시 시드 초기화 => 봇 사이클 제한
'''
if b_info['profit'] > 25 or b_info['last_profit'] < -2:
b_info['profit'] = 0
b_info['amount'] = b_info['last_amount']
'''
b_info['position'] = 'long'
b_info['position_price'] = info['target_price']
if is_swap:
b_info['position'] = 'None'
b_info['position_price'] = 0
self.db.bot_update_by_trade_data(b_info)
self.db.insert_trade_data_to_tb_trade(b_info)
self.send_msg_about_position(b_info, info, b_info['position'])
def short(self, b_info, is_swap=False):
time.sleep(1)
if str(b_info['position']).lower() == 'short':
return
# 단방향 봇은 무포시 숏포지션 로직 실행 안함
if str(b_info['position']).lower() == 'none' and b_info['trade_type'] == 'single':
return
profit = None
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
if b_info['mode'] == 'test':
price = self._get_last_order_price_by_position(b_info, 'short')
profit = 0
if b_info['position'] == 'long':
profit = bool(float(price) > float(b_info['position_price']))
# 봇 수익 타입별 포지션 지정
if b_info['trade_type'] == 'single':
b_info['position'] = 'None'
b_info['position_price'] = 0
elif b_info['trade_type'] == 'double':
b_info['position'] = 'short'
b_info['position_price'] = price
b_info['trade_cnt'] += 1
if profit: # 실 수익 시 봇 모드 변경 및 초기화
b_info['mode'] = 'service'
b_info['position'] = 'None'
b_info['position_price'] = 0
b_info['profit'] = 0
b_info['winrate'] = 0
b_info['trade_cnt'] = 0
b_info['win_cnt'] = 0
b_info['lose_cnt'] = 0
# 계정 자산(원화) 동기화
# amt = self._get_amount_from_exchange(b_info)
b_info['amount'] = 0
b_info['last_amount'] = 0
# b_info['amount'] = amt['KRW']
# b_info['last_amount'] = amt['KRW']
title = '[%s] %s - 모의트레이딩 종료 알림' % (e_name, i_name)
body = "\n".join('{} : {}'.format(key, value) for key, value in b_info.items())
self.send_email(title, body)
if is_swap:
b_info['position'] = 'None'
b_info['position_price'] = 0
self.db.bot_update_by_trade_data(b_info)
return
res, info = self.api_obj[e_name].order_short(i_name)
if res:
profit = 0
info['seeds'] = self.api_obj[e_name].get_all_seeds()
if float(b_info['last_amount']) != 0:
b_info['last_profit'] = round((float(info['seeds']) / float(b_info['last_amount']) - 1) * 100, 2)
b_info['last_amount'] = info['seeds']
if b_info['amount'] != 0:
# 수익률 산출
profit = round((float(info['seeds']) / float(b_info['amount']) - 1) * 100, 2)
else:
b_info['amount'] = info['seeds']
# profit = round((float(info['target_price']) / float(b_info['position_price']) - 1) * 100, 2)
# 포지션 종료 및 수익 데이터 저장
if b_info['position'] != 'None':
b_info['trade_cnt'] += 1
b_info['profit'] = profit
# 포지션 종료 시 승률 산출
if profit > 0:
b_info['win_cnt'] += 1
b_info['winrate'] = round((int(b_info['win_cnt'])/(int(b_info['win_cnt'])+int(b_info['lose_cnt'])))*100, 2)
# b_info['winrate'] = round((b_info['win_cnt'] / (b_info['trade_cnt'] / 2)) * 100, 2)
else:
b_info['lose_cnt'] += 1
# 수익 25프로 이상 시 시드 초기화 => 봇 사이클 제한
'''
if b_info['profit'] > 50 or b_info['last_profit'] < -2:
b_info['profit'] = 0
b_info['amount'] = b_info['last_amount']
'''
# 새 포지션 진입
b_info['position'] = 'short'
b_info['position_price'] = info['target_price']
if 'single' in b_info['trade_type']:
b_info['position'] = 'None'
b_info['position_price'] = 0
if is_swap:
b_info['position'] = 'None'
b_info['position_price'] = 0
self.db.bot_update_by_trade_data(b_info)
self.db.insert_trade_data_to_tb_trade(b_info)
self.send_msg_about_position(b_info, info, b_info['position'])
# 외부 클래스에서 봇 정보를 통해 강제 종료 로직
def force_close_position(self, b_info):
# set exchage obj
self._get_last_order_price_by_position(b_info, b_info['position'])
# force close position
self.close_position(b_info)
# DB 안의 상태값을 통해 현재의 포지션 정보 획득
# 거래소 API를 통해 현재의 포지션을 받아온다. => 추후 기능 추가
def close_position(self, b_info):
if str(b_info['position']).lower() == 'long':
self.short(b_info, True)
elif str(b_info['position']).lower() == 'short':
self.long(b_info, True)
time.sleep(5)
def send_msg_about_position(self, b_info, trade_info, position='None'):
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
if 'none' in str(position).lower() or position is None:
title = '[%s] %s - 포지션 종료(봇 수익률 : %s%%)' % (e_name, i_name, b_info['profit'])
else:
title = '[%s] %s - %s 포지션 진입' % (e_name, i_name, position.upper())
body = "\n".join('{} : {}'.format(key, value) for key, value in b_info.items())
return self.send_email(title, body)
def update_bot_position(self, b_info):
self.db.upsert_bot_data(b_info)
# Use Secret API
def _get_amount_from_exchange(self, b_info):
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
return self.api_obj[e_name].get_now_amount(i_name)
# api 키 조회
def _get_access_key(self, b_info):
return self.db.select_user_keys_by_id(b_info['user_id'])
def _get_last_order_price_by_position(self, b_info, position):
e_name = self._get_exchange_name_from_target_name(b_info['target'])
i_name = self._get_item_name_from_target_name(b_info['target'])
keys = self._get_access_key(b_info)
if not hasattr(self.api_obj, e_name):
# for test
# module = self.load_module_by_path('.'.join(['collectors', 'crypto', e_name]))
module = importlib.import_module('.'.join(['collectors', 'crypto', 'c_' + e_name]))
if keys[e_name]:
access = keys[e_name]['access']
secret = keys[e_name]['secret']
e_obj = getattr(module, str(e_name).capitalize())(access, secret)
else:
e_obj = getattr(module, str(e_name).capitalize())()
self.api_obj[e_name] = e_obj
# test end
# 호가 조회
return self.api_obj[e_name].get_last_price_from_orderbook(i_name, position)
def _get_item_name_from_target_name(self, target):
return target.split('_')[2]
def _get_exchange_name_from_target_name(self, target):
return target.split('_')[1]
def _get_trade_signal_by_data(self, data, bot_info):
# return 'long'
# return 'short'
strategy = json.loads(bot_info['strategy'])
res = Signal().get_signal_by_indicators(data, strategy)
print(res[-5:-1])
# test - check signal match
# for idx in range(len(res)):
# if len(strategy) is len(res[idx]):
# if all(res[idx]):
# print('long -', data.index[idx])
# 시그널 확인
# print(res);sys.exit(1);
if len(strategy) is len(res[-2]):
if all(res[-2]):
return 'long'
elif not any(res[-2]):
return 'short'
return None
# def load_module_by_path(self, module_name):
# mod = __import__('%s' % (module_name), fromlist=[module_name])
# return mod
def _get_price_data_by_info(self, info):
exchange_info = info['target'].split('_')
# set secret api to exchange object
keys = self._get_access_key(info)
# add dynamically path to os.path
if not BASE_DIR + '/' + exchange_info[0] in sys.path:
sys.path.append(BASE_DIR + '/' + exchange_info[0])
# module = self.load_module_by_path('.'.join(['collectors', exchange_info[0], 'c_'+exchange_info[1]]))
mod = importlib.import_module('.'.join(['collectors', exchange_info[0], 'c_'+exchange_info[1]]))
# if not exchange_info[1] in keys:
# key = {exchange_info[1]: {
# 'access': 'access',
# 'secret': 'secret'
# }}
# self.db.upsert_keys_to_user_row('javamon1174@gmail.com', key)
# keys = self._get_access_key(info)
if keys[exchange_info[1]]:
access = keys[exchange_info[1]]['access']
secret = keys[exchange_info[1]]['secret']
e_obj = getattr(mod, str(exchange_info[1]).capitalize())(access, secret)
else:
e_obj = getattr(mod, str(exchange_info[1]).capitalize())()
# set leverage to obj
e_obj.leverage = int(info['leverage'])
# set api obj(to instance variable)
self.api_obj[exchange_info[1]] = e_obj
r_data = e_obj.get_history_data(exchange_info[2], info['time_unit'], False)
if r_data is None:
return self._get_price_data_by_info(info)
return r_data # for test
# return r_data[-200:]
def bot_list_by_user_id(self, id):
return self.db.get_bots_by_user_id(id)
def get_users_id(self):
return self.db.select_all_users_id_for_trade()
if __name__ == "__main__":
print('Oh! my bot. Trading Started.')
t = Trader()
t.run()