시스템 트레이딩을 했었는데. 내 거래 내역을 조회용으로 Cloud-Hosted 웹 사이트에 올리곤 했었다. 실시간으로 쏘는 게 아니라 서버에 모아두고 한번에 대량 insert 했는데. 배치로 올리는 게 아니라 eyeball checking 한번 하고 버튼 눌러서 올렸다.

첫 번째 화면에서 거래한거 로우 데이터 출력하면. 좌측 하단 코너에 빨간 버튼 눌러서 자동 전처리하고. 한번 더 버튼을 눌러서 올리는 시스템이다. 그리고 가장 궁금한 게 그 날 얼마 벌었냐 였는데. 회사 퇴근하고 와서 확인하는 재미가 쏠쏠했다 ㅋㅋ 그래서 버튼 옆에 작게 일일 ROI도 표시되게 만듦.

거래량이 많고 간혹 불량 데이터도 있어서 전처리를 좀 하고 올렸다.
# Imports
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
import json
import jwt
import hashlib
import uuid
import requests
from pymongo import MongoClient
import json
import pymongo
import time
import datetime
import pytz
requests 모듈을 썼었는데 안되는 건 아니지만 짜증나게시리 API 문서하고도 잘 안맞아서 뭐가 문제인지 들여다보진 않고 urllib랑 같이 쓰다 지금은 완전 갈아탔다.
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = GUI()
GUI.show()
app.exec_()
프로그램 진입점도 적어주고.
class GUI(QWidget):
def __init__(self):
# QWidget 인스턴스 생성
super().__init__()
# 윈도우 사이즈 세팅
self.setGeometry(50, 30, 1800, 800)
# 윈도우 제목 세팅
self.setWindowTitle("데이터 이관")
# 기본 레이아웃 및 클래스 변수 생성
self.layout = QVBoxLayout(self)
# 레이아웃 적용
self.setLayout(self.layout)
# 몽고DB Connection 생성
self.connectDatabase()
# Table 그리기
self.createTable()
# 하단 메뉴 그리기
self.createTopBar()
def connectDatabase(self):
# 몽고DB Connection 생성
self.client = MongoClient('mongodb://localhost:27017/')
# 데이터베이스 레퍼런싱
self.local = self.client['local']
def createTable(self):
# Table 인스턴스 생성
self.table = QTableWidget()
# 컬럼 수 세팅
self.table.setColumnCount(7)
# 컬럼 명 세팅
self.table.setHorizontalHeaderLabels(['ID', 'Ticker', 'Price', 'Position', 'PnL', 'Size', 'Datetime'])
# 마지막 컬럼을 확장하여 넓이 채우기
self.table.horizontalHeader().setStretchLastSection(True)
# 컬럼 기본 넓이를 200으로 설정
self.table.horizontalHeader().setDefaultSectionSize(200)
# 셀을 클릭해도 전체 행이 선택하도록 설정
self.table.setSelectionBehavior(QTableView.SelectRows)
# 행 선택 시 focus / outline 숨김
self.table.setFocusPolicy(Qt.NoFocus)
# 테이블 데이터 채우는 메소드 호출
self.fillTable()
# 테이블 내용 편집 기능 비활성화
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
# 메인 레이아웃에 테이블 추가
self.layout.addWidget(self.table)
def createTopBar(self):
# Widget 인스턴스 생성
top_bar = QWidget()
# 레이아웃 생성
top_bar_layout = QHBoxLayout()
# PushButton 객체 생성
self.button = QPushButton("Stage")
# PushButton 넓이 100으로 고정
self.button.setFixedWidth(100)
# PushButton 높이 30으로 고정
self.button.setFixedHeight(30)
# PushButton CSS 적용
self.button.setStyleSheet("background-color: #FF5733; color: #FFFFFF; \
border: none; margin-right: 5px;")
# PushButton onClick 함수 세팅
self.button.clicked.connect(self.stageData)
# GroupBox 객체 생성
self.dailyROI = QGroupBox("일일 ROI")
# GroupBox 용 레이아웃 생성
top_bar_groupbox_layout = QVBoxLayout()
# Label 생성
self.dailyRoi = QLabel("")
# 레이아웃에 라벨 추가
top_bar_groupbox_layout.addWidget(self.dailyRoi)
# GroupBox에 레이아웃 적용
self.dailyROI.setLayout(top_bar_groupbox_layout)
# 위젯 레이아웃에 PushButton 추가
top_bar_layout.addWidget(self.button)
# 위젯 레이아웃에 GroupBox 추가
top_bar_layout.addWidget(self.dailyROI)
# 위젯에 레이아웃 적용
top_bar.setLayout(top_bar_layout)
# 메인 레이아웃에 위젯 추가
self.layout.addWidget(top_bar)
틀을 짜준다. 그리고 테이블 데이터를 채워준다.
def fillTable(self):
# 데이터 호출 --> self.data 객체에 담김
self.bringData()
# self.data의 개수만큼 테이블의 행 수 세팅
self.table.setRowCount(len(self.data))
# self.data를 루프하여 테이블에 추가
for i, x in enumerate(self.data):
# 몽고DB에서 _id는 bson.ObjectId로 되어 있기 때문에 str 변환
id = QTableWidgetItem(str(x['_id']))
# TableWidgetItem 인스턴스에 값을 매개 변수로 보내서 객체 생성
underlyingAsset = QTableWidgetItem(x['underlyingAsset'])
# datetime도 마찬가지로 Date/Time으로 되어 있기 때문에 str 변환
datetime = QTableWidgetItem(str(x['datetime']))
# tradePnl은 float이기 때문에 소수점 2자리 수 까지 있는 str으로 변환
tradePnl = QTableWidgetItem(f"{(x.get('tradePnl', 0)):,.2f}")
# -1이면 SELL 표시
if x['action'] == -1:
action = QTableWidgetItem("SELL")
size = QTableWidgetItem(f"{(x['consumption']):,.2f}")
# 1이면 BUY 표시
elif x['action'] == 1:
action = QTableWidgetItem("BUY")
size = QTableWidgetItem("")
barePrice = QTableWidgetItem(f"{(x['barePrice']):,.2f}")
# 포지션 i 잡고 행에 셀마다 값 매핑
self.table.setItem(i, 0, id)
self.table.setItem(i, 1, underlyingAsset)
self.table.setItem(i, 2, barePrice)
self.table.setItem(i, 3, action)
self.table.setItem(i, 4, tradePnl)
self.table.setItem(i, 5, size)
self.table.setItem(i, 6, datetime)
def bringData(self):
# 빈 배열 생성
self.data = []
# 로컬 변수 생성
tickers = []
unclosed = []
# 콜렉션 레퍼런싱
mycol = self.local['performance']
# 쿼리 실행
self.data = list(mycol.find().sort("datetime", pymongo.DESCENDING))
가장 중요한 버튼 콜백 로직 붙이기.
def stageData(self):
# 스테이징 할 데이터가 없으면 빠꾸줌
if len(self.data) == 0:
return
# 조건별로 데이터 필터 걸기 (if/else 가 많아서 잘 모르겠음.. ㅈㅅ)
self.data_new = []
curr_pos = 0
curr_ticker = self.data[0]['underlyingAsset']
for i, d in enumerate(self.data):
if d['action'] == -1:
self.data_new.append(d)
curr_pos = -1
elif curr_pos == -1:
self.data_new.append(d)
curr_pos = 1
elif curr_ticker != d['underlyingAsset']:
self.data_new.append(d)
elif i != len(self.data) - 1 and \
self.data[i+1]['underlyingAsset'] == curr_ticker and \
self.data[i+1]['action'] == -1:
self.data_new.append(d)
curr_ticker = d['underlyingAsset']
# Table 내용 바꾸기
self.replaceTable()
# PushButton 내용 바꾸기
self.replaceButton()
# GroupBox 내용 바꾸기
self.updateROI()
def updateROI(self):
"""
일일 ROI 계산 후 UI 상에 업데이트
"""
tradePnl = 0
balance = []
for d in self.data_new:
tradePnl += float(d.get('tradePnl', 0))
balance.append(float(d['balance']))
dailyRoi = (tradePnl / (sum(balance) / len(balance))) * 100
self.dailyRoi.setText(str(dailyRoi))
def replaceButton(self):
"""
빨간 Stage 버튼에서 녹색 Send 버튼으로 바꾸고 onClick 액션 교체
"""
self.button.setText("Send")
self.button.setStyleSheet("background-color: #006400; color: #FFFFFF; \
border: none; margin-left: 5px;")
self.button.clicked.connect(self.sendData)
def replaceTable(self):
"""
staging 버튼 눌러서 전처리된 data_new 배열대로 테이블 다시 그리기
"""
self.table.setRowCount(len(self.data_new))
for i, x in enumerate(self.data_new):
id = QTableWidgetItem(str(x['_id']))
underlyingAsset = QTableWidgetItem(x['underlyingAsset'])
datetime = QTableWidgetItem(str(x['datetime']))
tradePnl = QTableWidgetItem(f"{float(x.get('tradePnl', 0)):,.2f}")
if x['action'] == -1:
action = QTableWidgetItem("SELL")
size = QTableWidgetItem(f"{float(x['consumption']):,.2f}")
elif x['action'] == 1:
action = QTableWidgetItem("BUY")
size = QTableWidgetItem("")
barePrice = QTableWidgetItem(f"{float(x['barePrice']):,.2f}")
self.table.setItem(i, 0, id)
self.table.setItem(i, 1, underlyingAsset)
self.table.setItem(i, 2, barePrice)
self.table.setItem(i, 3, action)
self.table.setItem(i, 4, tradePnl)
self.table.setItem(i, 5, size)
self.table.setItem(i, 6, datetime)
def sendData(self):
"""
쿼리 스트링을 짜서 바로 보내는 거라 보안 상 취약하지만 혼자 쓰는 거라 노 상관
"""
data = list()
for row in self.data_new:
x = "('" + str(row['datetime']) + "', '" + str(row['productType']) + "', '" + str(row['tradePnl']) + "', \
'" + str(row['underlyingAsset'].split("-")[1]) + "', '" + str(row['action']) + "', '" + str(row['barePrice']) + "', '" + str(row['consumption']) + "', \
'" + str(row['balance']) + "', '" + str(row['totalPrice']) + "', '" + str(row['_id']) + "'),"
data.append(x)
result = requests.post(url="https://www.dongkye.tech/py-gui/postData.php", data={'data': "".join(data)[:-1]})
if result.json()['result'] == "success":
# 이미 처리한 데이터는 performance 콜렉션에서 history 콜렉션으로 복제하는 메소드 호출
if self.duplicateData() == True:
# 복제 성공 시 performance 콜렉션에서는 데이터 삭제
self.deleteData()
# 테이블 다시 렌더링
self.fillTable()
# 복제 실패 시 에러를 Dialog 창으로 띄움
else:
self.localErrorMessage()
# 외부 시스템 이관 실패 시 에러를 Dialog 창으로 띄움
else:
self.errorMessage()
def duplicateData(self):
"""
데이터 복제
"""
collection = self.local['history']
x = collection.insert_many(self.data)
return x.acknowledged
def deleteData(self):
"""
데이터 삭제. delete_many도 사용 가능
"""
collection = self.local['performance']
for x in self.data:
collection.delete_one(x)
Dialog 부분. 코드가 겹치기 때문에 리펙토링 가능.
def localErrorMessage(self, err_code):
dlg = QMessageBox(self)
dlg.setWindowTitle("로컬 네트워크 오류")
dlg.resize(QSize(500, 100))
dlg.setText("로컬 서버에서 [이관] 오류가 생겼습니다.")
dlg.addButton(QPushButton('확인'), QMessageBox.YesRole)
dlg.setIcon(QMessageBox.Information)
dlg.exec()
def errorMessage(self):
dlg = QMessageBox(self)
dlg.setWindowTitle("네트워크 오류")
dlg.resize(QSize(500, 100))
dlg.setText("오류가 발생했습니다.")
dlg.addButton(QPushButton('확인'), QMessageBox.YesRole)
dlg.setIcon(QMessageBox.Information)
dlg.exec()
얘는 구 버전 스샷 떠논거

우측 하단 코너에 Last Refresh 저런 쓰잘떼기 없는 거 다 뺀 거. 집에서나 밖에서나 하루종일 쳐다보고 있는 것도 아닌데 있어보이는 거 그냥 갖다 붙인거다. 기존에는 누적 수익이랑 수익률, 수익금 등 별의 별거 다 써놨는데 지저분해서 필요한 ROI 하나 남겨둔 거다.
그리고 기존에 일일이 클릭해서 스테이징 넘겼는데 데이터가 많아지면서 귀찮아서 생략한 것. 근데 지금 보니까 색감 이쁜듯.