import pandas as pd
kquant API document (0.3.6)
backtest_stock_port_daily
주식 포트폴리오의 일간기준 백테스트 수행
- 일간 종가정보 기반
- 하루에 한 번, 종가 기준으로 체결
- 동일 종목의 매수와 매도를 당일에 같이 하는 것은 불가능
- 실현손익은 선입선출(FIFO: First-In-First-Out)방식으로 계산
backtest_stock_port_daily('Union[pd.DataFrame, Callable[[dt.datetime, dict[str, pd.DataFrame], dict[str, pd.DataFrame], logging.Logger], list[tuple[str, int]]]]',
order: 'DATE_IN'=None,
start_date: 'DATE_IN'=None,
end_date: 'int'=0,
init_cash: 'bool'=False,
close_on_end: 'bool'=False,
allow_loan: 'float'=0.0,
broker_fee_percent: 'float'=0.0,
exchange_fee_percent: 'float'=0.0,
trade_tax_percent: 'int'=0,
slippage_tick: 'bool'=False,
return_position: 'bool'=False,
use_notadj: 'Optional[logging.Logger]'=None,
logger: 'Optional[str]'=None,
logname: 'Optional[str]'=None,
logpath: 'Optional[int]'=None,
loglevel: 'Optional[bool]'=False,
return_logger: -> Union[dict[str, pd.DataFrame], tuple[dict[str, pd.DataFrame], dict[str, pd.DataFrame]], tuple[dict[str, pd.DataFrame], logging.Logger], tuple[dict[str, pd.DataFrame], dict[str, pd.DataFrame], logging.Logger]] )
order
(Union[pd.DataFrame, Callable]
): 주식 주문 정보를 포함하는 데이터프레임 혹은 함수.order 인수가 데이터프레임인 경우 다음과 같은 열을 포함하고 있어야 합니다.
- “DATE”: 날짜 열.
daily_stock
함수로 출력된 데이터프레임과 마찬가지로datetime64[ns]
형식이거나pd.to_datetime
함수로 변환가능한 형식의 문자열 - “SYMBOL”: 주식 종목 단축코드 문자열
- “ORDER”: 주문수량 열 (양수는 매수, 음수는 매도)
order 인수가 함수인 경우 다음과 같은 형식이어야 합니다.
import pandas as pd import datetime as dt import logging def trade_func( date: dt.date, dict_df_result: dict[str,pd.DataFrame], dict_df_position: dict[str,pd.DataFrame], logger: logging.Logger, ) -> list[tuple[str,int]]: symbols_and_orders = [ ("주문주식단축코드1",주문수량1), ("주문주식단축코드2",주문수량2), ... ] return symbols_and_orders
- “DATE”: 날짜 열.
start_date
(DATE_IN
): 백테스트 시작 날짜 문자열. None이면 order 데이터프레임 날짜 중 가장 첫번째 날짜를 이용.end_date
(DATE_IN
): 백테스트 종료 날짜 문자열 None이면 order 데이터프레임 날짜 중 가장 마지막 날짜를 이용.init_cash
(int
): 초기 보유현금. 디폴트 0원close_on_end
(bool
): True면 백테스트 마지막날에는 매매정보를 무시하고 항상 보유잔고 정리. 디폴트 Falseallow_loan
(bool
): 융자가능여부 즉, 음수인 보유현금 허용 설정값. 디폴트 Falsebroker_fee_percent
(float
): 증권사 수수료(%). 디폴트 0exchange_fee_percent
(float
): 유관기관 수수료(%). 디폴트 0trade_tax_percent
(float
): 매도시 주식양도세(%). 디폴트 0slippage_tick
(int
): 거래시 발생하는 슬리피지(slippage)틱(호가가격단위), 디폴트 0return_position
(bool
): 포지션 정보 데이터프레임을 반환할지의 여부. 디폴트 Falseuse_notadj
(bool
): 수정주가가 아닌 원주가 사용. 디폴트 Falselogger
(Optional[logging.Logger]
): 로거 객체. None이면 자동생성logname
(Optional[str]
): 로거 이름. 로거 객체 logger가 None이면 이 이름으로 생성logpath
(Optional[str]
): 로그파일 경로 문자열. None이 아니면 로그파일 생성loglevel
(Optional[int]
): 로그레벨. 디폴트 logging.INFO :param Optional[bool] return logger: True이면 로거 객체를 추가적으로 반환반환값 (
pd.DataFrame
): 매매 결과를 포함하는 데이터프레임.매매 결과 데이터프레임은 다음과 같은 열을 포함
- DATE: 날짜
- SYMBOL: 종목단축코드
- PRICE: 주식 평가를 위한 당일 종가
- ORDER: 주문수량, 양수이면 매수, 음수이면 매도
- QTY: 실제 매매수량, 양수이면 매수, 음수이면 매도
- TRADE_PRICE: 체결 가격, 매매일 종가에서 슬리피지(slippage)만큼 손실을 보면서 체결
- FEE: 증권사 및 유관기관 수수료 금액
- TRADE_TAX: 매도시 발생하는 증권거래세 금액
- SLIPPAGE: 주식의 현재 가격과 실제 매매 가격의 차이에 의해 발생하는 슬리피지(slippage)
- CASHFLOW: 현금흐름, 양수이면 매도시 발생하는 현금유입, 음수이면 매수시 발생하는 현금유출
- CASH: 당일의 보유 현금 금액
- POSITION: 당일의 보유 주식 수량
- AVG_PRICE: 당일기준 보유 주식의 역사적 평균가격, 선입선출 방식으로 계산
- HIST_VALUE: 보유 주식의 매수 금액, 선입선출 방식으로 계산
- STOCK_VALUE: 당일의 주식 평가액
- TOTAL_VALUE: 당일의 주식 평가액과 현금 보유액의 합계
- REAL_PROFIT: 주식 매도시 발생하는 실현손익, 음수이면 손실
- UNREAL_PROFIT: 보유 주식에 대한 평가손익, 음수이면 손실
- PROFIT: 총손익, 누적 실현손익과 최종 평가손익의 합, 음수이면 손실
만약 return_position = True 인 경우 다음과 같은 포지션 정보 데이터프레임 추가
- DATE: 체결 날짜
- SYMBOL: 종목단축코드
- QTY: 현재 보유 수량
- TRADE_PRICE: 체결 가격
- HIST_VALUE: 현재 보유 수량의 역사적 가치
- FEE: 현재 보유 수량에 해당하는 수수료 합계
- NOT_DELETE: 내부적으로 사용하는 필드
예제 코드
= pd.DataFrame({
df_order "DATE": "20230102",
"SYMBOL": ["005930","000660"],
"ORDER": [10,10],
})
df_order
DATE | SYMBOL | ORDER | |
---|---|---|---|
0 | 20230102 | 005930 | 10 |
1 | 20230102 | 000660 | 10 |
import kquant as kq
= kq.backtest_stock_port_daily(
dict_df_result
df_order,=100_000_000,
init_cash )
[2023-01-02] 종목: 005930, 주문전 보유수량: 0 주문수량: 10, 매매수량: 10, 주문후 보유수량: 10
[2023-01-02] 종목: 000660, 주문전 보유수량: 0 주문수량: 10, 매매수량: 10, 주문후 보유수량: 10
= list(dict_df_result.keys()) symbols
symbols
['005930', '000660', 'TOTAL']
= dict_df_result["005930"] df_result_005930
df_result_005930
DATE | SYMBOL | PRICE | ORDER | QTY | TRADE_PRICE | POSITION | AVG_PRICE | FEE | TRADE_TAX | SLIPPAGE | CASHFLOW | CASH | HIST_VALUE | STOCK_VALUE | TOTAL_VALUE | REAL_PROFIT | UNREAL_PROFIT | PROFIT | HIGHWATERMARK | DRAWDOWN | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2023-01-02 | 005930 | 55,500 | 10 | 10 | 55,500 | 10 | 55,500.0000 | 0 | 0 | 0 | -555,000 | -555,000 | 555,000 | 555,000 | 0 | 0 | 0 | 0 | 0 | 0 |
= dict_df_result["TOTAL"] df_result_total
df_result_total
DATE | SYMBOL | PRICE | ORDER | QTY | TRADE_PRICE | POSITION | AVG_PRICE | FEE | TRADE_TAX | SLIPPAGE | CASHFLOW | CASH | HIST_VALUE | STOCK_VALUE | TOTAL_VALUE | REAL_PROFIT | UNREAL_PROFIT | PROFIT | HIGHWATERMARK | DRAWDOWN | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2023-01-02 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | -1,312,000 | 98,688,000 | 1,312,000 | 1,312,000 | 100,000,000 | 0 | 0 | 0 | 100,000,000 | 0 |
from dateutil.parser import parse
import datetime as dt
import logging
def trade_func(
date: dt.date,dict[str,pd.DataFrame],
dict_df_result: dict[str,pd.DataFrame],
dict_df_position:
logger: logging.Logger,-> list[tuple[str,int]] | None:
) if date == parse("20230102").date():
return [("005930",10),("000660",10)]
else:
return []
= kq.backtest_stock_port_daily(
dict_df_result
trade_func,"20230102",
"20230106",
=100_000_000,
init_cash )
[2023-01-02] 종목: 005930, 주문전 보유수량: 0 주문수량: 10, 매매수량: 10, 주문후 보유수량: 10
[2023-01-02] 종목: 000660, 주문전 보유수량: 0 주문수량: 10, 매매수량: 10, 주문후 보유수량: 10
[2023-01-03] 종목: 005930, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-04] 종목: 005930, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-05] 종목: 005930, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-06] 종목: 005930, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-03] 종목: 000660, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-04] 종목: 000660, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-05] 종목: 000660, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
[2023-01-06] 종목: 000660, 주문전 보유수량: 10 주문수량: 0, 매매수량: 0, 주문후 보유수량: 10
= dict_df_result["005930"] df_result_005930
df_result_005930
DATE | SYMBOL | PRICE | ORDER | QTY | TRADE_PRICE | POSITION | AVG_PRICE | FEE | TRADE_TAX | SLIPPAGE | CASHFLOW | CASH | HIST_VALUE | STOCK_VALUE | TOTAL_VALUE | REAL_PROFIT | UNREAL_PROFIT | PROFIT | HIGHWATERMARK | DRAWDOWN | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2023-01-02 | 005930 | 55,500 | 10 | 10 | 55,500 | 10 | 55,500.0000 | 0 | 0 | 0 | -555,000 | -555,000 | 555,000 | 555,000 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2023-01-03 | 005930 | 55,400 | 0 | 0 | 0 | 10 | 55,500.0000 | 0 | 0 | 0 | 0 | -555,000 | 555,000 | 554,000 | -1,000 | 0 | -1,000 | -1,000 | 0 | 1,000 |
2 | 2023-01-04 | 005930 | 57,800 | 0 | 0 | 0 | 10 | 55,500.0000 | 0 | 0 | 0 | 0 | -555,000 | 555,000 | 578,000 | 23,000 | 0 | 23,000 | 23,000 | 23,000 | 0 |
3 | 2023-01-05 | 005930 | 58,200 | 0 | 0 | 0 | 10 | 55,500.0000 | 0 | 0 | 0 | 0 | -555,000 | 555,000 | 582,000 | 27,000 | 0 | 27,000 | 27,000 | 27,000 | 0 |
4 | 2023-01-06 | 005930 | 59,000 | 0 | 0 | 0 | 10 | 55,500.0000 | 0 | 0 | 0 | 0 | -555,000 | 555,000 | 590,000 | 35,000 | 0 | 35,000 | 35,000 | 35,000 | 0 |
= dict_df_result["TOTAL"] df_result_total
df_result_total
DATE | SYMBOL | PRICE | ORDER | QTY | TRADE_PRICE | POSITION | AVG_PRICE | FEE | TRADE_TAX | SLIPPAGE | CASHFLOW | CASH | HIST_VALUE | STOCK_VALUE | TOTAL_VALUE | REAL_PROFIT | UNREAL_PROFIT | PROFIT | HIGHWATERMARK | DRAWDOWN | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2023-01-02 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | -1,312,000 | 98,688,000 | 1,312,000 | 1,312,000 | 100,000,000 | 0 | 0 | 0 | 100,000,000 | 0 |
1 | 2023-01-03 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | 0 | 98,688,000 | 1,312,000 | 1,310,000 | 99,998,000 | 0 | -2,000 | -2,000 | 100,000,000 | 2,000 |
2 | 2023-01-04 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | 0 | 98,688,000 | 1,312,000 | 1,388,000 | 100,076,000 | 0 | 76,000 | 76,000 | 100,076,000 | 0 |
3 | 2023-01-05 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | 0 | 98,688,000 | 1,312,000 | 1,396,000 | 100,084,000 | 0 | 84,000 | 84,000 | 100,084,000 | 0 |
4 | 2023-01-06 | TOTAL | 0 | 0 | 0 | 0 | 20 | 0.0000 | 0 | 0 | 0 | 0 | 98,688,000 | 1,312,000 | 1,421,000 | 100,109,000 | 0 | 109,000 | 109,000 | 100,109,000 | 0 |