추천 게시물

bitget 수익률 계산기 v0

목차

비트겟에서 운영 중인 봇의 수익률을 계산하는 간단한 계산기를 만들어 보았다.


balance와 position pnl을 바탕으로 수익률을 계산해 준다. 파일은 그래서 2개가 필요하다.


import pandas as pd
import re
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.text import Text
import platform

# 운영체제에 따른 폰트 설정
if platform.system() == "Windows":
plt.rcParams['font.family'] = 'Malgun Gothic'
elif platform.system() == "Darwin":
plt.rcParams['font.family'] = 'AppleGothic'
else:
plt.rcParams['font.family'] = 'NanumGothic'
plt.rcParams['axes.unicode_minus'] = False

# 전역 변수: 원본 데이터와 분석된 데이터를 저장
trade_df_original = None
asset_df_original = None
processed_data = None


# --- 1. 데이터 처리 및 계산 함수들 ---
def extract_coin_name(futures_string):
if pd.isna(futures_string): return None
return futures_string.split()[0]


def extract_number(text):
if pd.isna(text): return None
result = re.findall(r"[-+]?\d*\.?\d+", str(text))
return float(result[0]) if result else None


def calculate_metrics(df):
"""주어진 데이터프레임에 대해 수익률 및 수익금을 계산"""
# 자산(Wallet balance) 0이거나 없는 경우를 대비하여 0으로 나누는 오류 방지
df['return'] = (df['pnl_clean'] / df['Wallet balance']).fillna(0)
df['cum_return'] = df.groupby('coin')['return'].transform(lambda x: (1 + x).cumprod() - 1)
df['cum_pnl'] = df.groupby('coin')['pnl_clean'].transform(lambda x: x.cumsum())
return df


# --- 2. GUI와 연결된 핵심 함수들 ---
def load_files(file_type):
"""거래 내역 또는 자산 내역 파일을 불러오는 공용 함수"""
global trade_df_original, asset_df_original

file_paths = filedialog.askopenfilenames(
title=f"{file_type} 파일을 선택하세요",
filetypes=(("CSV & 엑셀", "*.csv *.xlsx *.xls"), ("모든 파일", "*.*"))
)
if not file_paths: return

try:
df_list = [pd.read_csv(f) if f.endswith('.csv') else pd.read_excel(f) for f in file_paths]
combined_df = pd.concat(df_list, ignore_index=True)

rows_before = len(combined_df)
combined_df.drop_duplicates(inplace=True)

if file_type == '거래내역':
combined_df['pnl_clean'] = combined_df['Position Pnl'].apply(extract_number)
combined_df['Opening time'] = pd.to_datetime(combined_df['Opening time'])
combined_df['close_time_dt'] = pd.to_datetime(combined_df['Closed time'])
combined_df['coin'] = combined_df['Futures'].apply(extract_coin_name)
trade_df_original = combined_df
trade_files_label.config(text=f"{len(file_paths)}개 파일 로드됨 ({len(trade_df_original)}개 내역)")
elif file_type == '자산내역':
# 'balance_...' 파일의 컬럼 이름에 맞게 수정
combined_df.rename(columns={'Date': 'asset_time', 'Wallet balance': 'asset_balance'}, inplace=True)
combined_df['asset_time'] = pd.to_datetime(combined_df['asset_time'])
combined_df['asset_balance'] = pd.to_numeric(combined_df['asset_balance'], errors='coerce')
asset_df_original = combined_df.dropna(subset=['asset_time', 'asset_balance'])
asset_files_label.config(text=f"{len(file_paths)}개 파일 로드됨 ({len(asset_df_original)}개 내역)")

messagebox.showinfo("성공", f"{file_type} 파일 로드 완료!\n(중복 {rows_before - len(combined_df)}개 제거)")

if trade_df_original is not None and asset_df_original is not None:
run_button.config(state=tk.NORMAL)

except Exception as e:
messagebox.showerror("파일 오류", f"{file_type} 파일을 읽는 중 오류가 발생했습니다:\n{e}")


def run_analysis():
global processed_data, trade_df_original, asset_df_original

if trade_df_original is None or asset_df_original is None:
messagebox.showwarning("데이터 없음", "거래내역과 자산내역 파일을 모두 불러와주세요.")
return

trades = trade_df_original.copy()
assets = asset_df_original.copy()

is_long = long_var.get() == 1
is_short = short_var.get() == 1
start_str = start_date_entry.get()
end_str = end_date_entry.get()

if not is_long and not is_short: messagebox.showwarning("선택 오류", "Long/Short 포지션 중 하나 이상을 선택해주세요."); return
try:
pd.to_datetime(start_str); pd.to_datetime(end_str)
except ValueError:
messagebox.showerror("입력 오류", "날짜 형식이 잘못되었습니다."); return

if is_long and not is_short:
trades = trades[trades['Futures'].str.contains("Long", case=False, na=False)]
elif not is_long and is_short:
trades = trades[trades['Futures'].str.contains("Short", case=False, na=False)]

trades = trades[(trades['close_time_dt'] >= start_str) & (trades['close_time_dt'] <= end_str)]

if trades.empty:
messagebox.showwarning("데이터 없음", "선택된 조건에 해당하는 거래내역이 없습니다.")
processed_data = None
else:
# --- 시간 기준 데이터 병합 ---
trades = trades.sort_values('Opening time')
assets = assets.sort_values('asset_time')
merged_df = pd.merge_asof(
trades,
assets[['asset_time', 'asset_balance']],
left_on='Opening time',
right_on='asset_time',
direction='backward'
)
merged_df.rename(columns={'asset_balance': 'Wallet balance'}, inplace=True)
merged_df.dropna(subset=['Wallet balance'], inplace=True) # 자산 정보가 없는 거래는 제외

if merged_df.empty:
messagebox.showwarning("데이터 없음", "거래내역에 해당하는 자산 정보를 찾을 수 없습니다.\n자산 데이터의 날짜 범위를 확인해주세요.")
processed_data = None
else:
processed_data = calculate_metrics(merged_df.sort_values('close_time_dt'))

update_summary_table(processed_data)
if processed_data is not None:
plot_frame.config(text=f"그래프 보기 ({len(processed_data)}개 거래 기준)")
return_plot_button.config(state=tk.NORMAL);
pnl_plot_button.config(state=tk.NORMAL)
else:
plot_frame.config(text="그래프 보기");
return_plot_button.config(state=tk.DISABLED);
pnl_plot_button.config(state=tk.DISABLED)


def update_summary_table(df):
for item in summary_tree.get_children(): summary_tree.delete(item)
if df is None or df.empty: return

summary_df = df.groupby('coin').last().reset_index()
for _, row in summary_df.iterrows():
summary_tree.insert("", "end", values=(row['coin'], f"{row['cum_return'] * 100:.2f}%", f"{row['cum_pnl']:.2f}"))

if not df.empty:
total_pnl = df['pnl_clean'].sum()
total_return = ((1 + df['return']).prod() - 1)
summary_tree.insert("", "end", values=("-" * 12, "-" * 18, "-" * 18))
summary_tree.insert("", "end", values=("총합 (Total)", f"{total_return * 100:.2f}%", f"{total_pnl:.2f}"),
tags=('total_row',))


# 시각화 함수
def create_plot(df, y_column, title, y_label):
if df is None or df.empty: messagebox.showinfo("정보", "그래프를 그릴 데이터가 없습니다."); return
fig, ax = plt.subplots(figsize=(15, 8));
for coin, group in df.groupby('coin'):
ax.plot(group['close_time_dt'], group[y_column], label=coin, picker=True, pickradius=5, lw=1.5)
leg = ax.legend(title="코인 목록");
[lbl.set_picker(True) for lbl in leg.get_texts()]

def on_pick(event):
artist = event.artist;
target_label = None
if isinstance(artist, Line2D):
target_label = artist.get_label()
elif isinstance(artist, Text):
target_label = artist.get_text()
if target_label is None: return
for line in ax.get_lines():
if line.get_label() == target_label:
line.set_linewidth(3.0); line.set_alpha(1.0)
else:
line.set_linewidth(1.0); line.set_alpha(0.3)
ax.set_title(f"{title} (선택: {target_label})", fontsize=16)
new_leg = ax.legend(title="코인 목록");
[lbl.set_picker(True) for lbl in new_leg.get_texts()]
fig.canvas.draw()

fig.canvas.mpl_connect('pick_event', on_pick)
ax.set_title(f"{title} (라인 또는 범례를 클릭하여 강조)", fontsize=16)
ax.set_xlabel('종료 시간 (Close Time)');
ax.set_ylabel(y_label);
ax.grid(True);
fig.tight_layout()
plt.show()


# --- GUI 레이아웃 ---
root = tk.Tk()
root.title("수익 분석기 v6.0 (자산 기준 수익률)")
root.geometry("550x650")

main_frame = tk.Frame(root);
main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)

# 1. 파일 불러오기
load_file_frame = tk.LabelFrame(main_frame, text="1. 파일 불러오기")
load_file_frame.pack(fill=tk.X, pady=5)
trade_file_frame = tk.Frame(load_file_frame)
trade_file_frame.pack(fill=tk.X, padx=5, pady=5)
tk.Button(trade_file_frame, text="거래내역 파일 선택", command=lambda: load_files('거래내역')).pack(side=tk.LEFT)
trade_files_label = tk.Label(trade_file_frame, text="로드되지 않음", fg="grey")
trade_files_label.pack(side=tk.LEFT, padx=10)
asset_file_frame = tk.Frame(load_file_frame)
asset_file_frame.pack(fill=tk.X, padx=5, pady=5)
tk.Button(asset_file_frame, text="자산내역 파일 선택", command=lambda: load_files('자산내역')).pack(side=tk.LEFT)
asset_files_label = tk.Label(asset_file_frame, text="로드되지 않음", fg="grey")
asset_files_label.pack(side=tk.LEFT, padx=10)

# 2. 분석 조건 설정
condition_frame = tk.LabelFrame(main_frame, text="2. 분석 조건 설정")
condition_frame.pack(fill=tk.X, pady=5)
date_frame = tk.Frame(condition_frame);
date_frame.pack(fill=tk.X, pady=2)
tk.Label(date_frame, text="기간:").pack(side=tk.LEFT, padx=5)
start_date_entry = tk.Entry(date_frame);
start_date_entry.pack(side=tk.LEFT, padx=5);
start_date_entry.insert(0, "2024-01-01")
tk.Label(date_frame, text="~").pack(side=tk.LEFT, padx=2)
end_date_entry = tk.Entry(date_frame);
end_date_entry.pack(side=tk.LEFT, padx=5);
end_date_entry.insert(0, "2025-12-31")
position_frame = tk.Frame(condition_frame);
position_frame.pack(fill=tk.X, pady=2)
tk.Label(position_frame, text="포지션:").pack(side=tk.LEFT, padx=5)
long_var = tk.IntVar(value=1);
short_var = tk.IntVar(value=1)
tk.Checkbutton(position_frame, text="Long", variable=long_var).pack(side=tk.LEFT, padx=10)
tk.Checkbutton(position_frame, text="Short", variable=short_var).pack(side=tk.LEFT, padx=10)

# 3. 분석 실행
run_frame = tk.LabelFrame(main_frame, text="3. 분석 실행")
run_frame.pack(fill=tk.X, pady=5)
run_button = tk.Button(run_frame, text="분석 실행", command=run_analysis, state=tk.DISABLED)
run_button.pack(pady=10)

# 4. 최종 결과 요약
summary_frame = tk.LabelFrame(main_frame, text="4. 최종 결과 요약")
summary_frame.pack(fill=tk.BOTH, expand=True, pady=5)
summary_tree = ttk.Treeview(summary_frame, columns=("coin", "cum_return", "cum_pnl"), show="headings")
summary_tree.heading("coin", text="코인");
summary_tree.heading("cum_return", text="최종 누적 수익률");
summary_tree.heading("cum_pnl", text="최종 누적 수익금")
summary_tree.column("coin", width=100);
summary_tree.column("cum_return", width=120, anchor='e');
summary_tree.column("cum_pnl", width=120, anchor='e')
summary_tree.pack(fill=tk.BOTH, expand=True)
summary_tree.tag_configure('total_row', font=('Malgun Gothic', 9, 'bold'))

# 5. 그래프 보기
plot_frame = tk.LabelFrame(main_frame, text="5. 그래프 보기")
plot_frame.pack(fill=tk.X, pady=5)
return_plot_button = tk.Button(plot_frame, text="누적 수익률(%) 그래프", state=tk.DISABLED,
command=lambda: create_plot(processed_data, 'cum_return', '코인별 누적 수익률', '누적 수익률 (%)'))
return_plot_button.pack(side=tk.LEFT, padx=5, pady=5, expand=True)
pnl_plot_button = tk.Button(plot_frame, text="누적 수익금(PnL) 그래프", state=tk.DISABLED,
command=lambda: create_plot(processed_data, 'cum_pnl', '코인별 누적 수익금', '누적 수익금 (PnL)'))
pnl_plot_button.pack(side=tk.LEFT, padx=5, pady=5, expand=True)

root.mainloop()

댓글