목 차
1. 들어가며
2. 사전설명
1) 영웅문G에서 1분봉의 RSI 확인하는 방법
2) RSI 기본설정은 14일(분)
3. 코드 설명
4. 전체코드
5. 마치며
1. 들어가며
지난 글에서는 SendOrder 함수를 통해 주문하는 방법을 알아보았다. opw30011(주문가능수량) 조회 후 SendOrder 함수를 사용하면 진입/청산을 할 수 있을 것이다.
이번 글에서는 1분봉 종가 데이터를 활용하여 RSI 구하는 방법에 대해 알아보자. 지금은 기존 코드에 추가로 데이터를 작성하고 있다. 이번에도 기존코드에 RSI를 추가하여 작성할 것이다. RSI의 개념 및 구하는 방법은 아래 링크를 참고하면 된다. 기존에 설명하였으므로, 간략히 설명한다.
※ 해외선물 1분봉의 데이터에서 RSI를 구하는 방법은 이미 설명하였다.(https://springcoming.tistory.com/171)
※ 기존 설명한 코드에서 코드의 가독성을 위해 일부 변경하였다. 물론 기존 코드(위의 링크)를 사용하여도 rsi를 구하는데는 문제가 없다.
2. 사전설명
1) 영웅문G에서 1분봉의 RSI 확인하는 방법
RSI 보조지표는 1분봉 종가를 활용한다. 영웅문G 화면번호 0603(종목의 차트조회)에서 RSI 72.69를 확인할 수 있다.
2) RSI 기본설정은 14일(분)
키움증권 영웅문G의 1분봉을 산출할 때 적용되는 기간(Period)는 14일(분)이다. 필자는 1분봉을 활용할 것이므로 아래에서 이루어질 설명의 기간은 14분이다. (영웅문G에서 1분봉 RSI에 최초 설정된 값은 14이다)
기간(14분)의 의미는? RSI(Relative Strength Index, 상대강도지수)이며, 주가(종가)의 평균 상승 변화량과 평균 하락 변화량을 비교하여 상대적인 강도를 뜻하는 지수이다. 생각해보아야 할 내용은 주가(종가) 평균 "상승 변화량"과 평균 "하락 변화량"을 말한다.
3. 코드 설명
로그인, 라이브러리 등 중복된 내용은 설명을 생략한다.
20줄~29줄 : 150줄~250줄에서 종가 및 rsi 등을 담을 리스트를 미리 선언한다.
37줄~43줄 : 256줄의 "데이터 입력"으로 인해 실행되는 함수를 정의한다. 256줄에서는 "종목코드, 분봉 종료"를 입력해주었다.(입력된 데이터 : NQZ23, 1)
74줄 ~80줄 : 키움측에서 제공한 데이터를 받을 함수이다.
76줄~77줄 : < 그림2-3 >에서 짤렸지만, rsi를 구하기 위해서는 각 분의 "종가"가 필요(76줄)하다. 즉 76줄은 키움측에서 제공받은 종가의 갯수(getrepeatcnt)를 나타낸다. 여기서 76줄은 int형으로 600개을 뜻한다.
79줄 : 42줄~43줄에서 데이터를 입력/요청하여서 "대기(loop)" 중인 상태를 멈추고(exit), 다음 코드를 진행시킨다.
80줄 : 151줄 함수를 실행한다.
※ 여기서 잠깐! 생각해볼 문제가 있다.
79줄(exit)를 먼저 쓰느냐, 80줄(함수 실행)을 먼저 쓰느냐에 대해 고민해 보았다.
필자는 원래 80줄의 내용 작성후 79줄 함수를 작성하였다.
그런데, 코드가 길어지다보니, 데이터를 받아와서 지표를 만들 때, 너무 빨리 코드가 진행되는 바람에 "지표"를 정의하지 못해서 "에러"가 발생하는 경우가 많았다.
→ 결론 : 키움증권에서 데이터를 받아올 때까지 대기(loop) 후 지표를 만드는 습관을 들이자.
여기서부터는 자세히 설명한다.
rsi는 2가지 계산으로 이루어진다. 1차(산술평균) 및 2차(가중평균)으로 구한다.
1차 산술평균은 151줄~192줄까지 설명이고, 2차 가중평균은 194줄~236줄까지 설명된다.
< 1차 계산 >
151줄 : rsi를 구하기 위해 함수를 선언한다.
152줄 : 76~77줄에서 선언한 데이터 갯수(getrepeatcnt)를 넣는다. (600개를 받아온다)
153줄~154줄 : 각각의 종가를 154줄(self.price_rsi_total) 리스트에 넣는다.
156줄 : self.price_rsi_total은 "현재 → 과거"의 시간 역순으로 배열되어 있으므로, 정배열(과거 → 현재)로 리스트의 순서를 바꾸어준다.
157줄 : self.price_rsi_total은 더이상 활용하지 않으므로, 리스트 초기화 시켜준다.
159줄~161줄 : 14개의 종가 변화량을 구하기 위해서는 15개의 종가가 필요하다. 각각의 종가를 161줄 self.close_price_rsi_change의 리스에 담는다.
163~164줄 : 이곳이 약간 어렵다.
163줄 : rsi는 "이전 분의 종가 - 현재 종가"이다. 161줄의 self.close_price_rsi_change에는 15개의 종가가 들어있다.
len(self.close_price_rsi_change)는 15를 뜻하므로, 163줄을 풀어서 써보면, for j in range(15-1)가 된다. 즉 for j in range(14)가 되는 것이다. 이것은 0~13을 의미한다.
164~165줄 : "이전 분의 종가 - 현재 종가"(=변화량)을 구하여 165줄의 self.close_price_third 리스트에 담는다.
16줄 : self.close_price_rsi_change 리스트는 더이상 활용하지 않으므로 초기화한다.
171줄~176줄 : "종가 변화량"이 양수일때, "변화량"이 0보다 작거나 같은 경우 0으로 self.early_au1 리스트에 담고, 0보다 크면 양수 그 자체로 self.early_au1 리스트에 담는다.
178줄~183줄 : "종가 변화량"이 음일때, "변화량"이 0보다 작거나 같은 경우 0으로 음수 그 자체로 self.early_ad1 리스트에 담고, 0보다 크거나 같으면 0을로 self.early_ad1 리스트에 담는다.
185줄~192 : 양수 및 음수 각각의 모음의 평균을 구하고, 각각의 리스트를 초기화한다.
< 2차 계산 >
첫번째 rsi는 15개 종가의 변화량을 산술평균하여 구했다. 2번째 rsi부터는 "앞의 값의 au와 ad"를 활용하여 구한다.
193~196줄 : "이전 분의 종가 - 현재분의 종가"를 지속적으로 구해준다.
198~203줄 : 종가 변화량이 양수 or 음수냐에 따라 적용할 결과값이 다르다.
205줄~26줄 : 198줄~203줄의 양수 혹은 음수에 따라 au8 및 ad8의 값이 달라진다.
208줄 : au/ad를 통해 rs를 구한다.
210줄 : rs를 통해 rsi를 구한다.
213줄 : 이번에 추가한 내용이다. rsi를 self.rsi_total_list 리스트에 담는다. 왜 리스트에 담는지는 228줄~236줄에서 설명한다.
220줄 : self.rsi_total_list는 정배열(과거 → 현재)이다. 역배열(현재 → 과거)바꾸어주었다. 이유는? 리스트의 0번이 현재값이다. 현재값의 rsi를 쉽게 구하기 위해 역배열하였다.
228줄~236줄 : 213줄에서 각각의 rsi를 추가하는 이유가 여기에서 설명된다.
지금의 리스트는 [현재, 현재-1분, 현재-2분, 현재-3분....]으로 이루어진다.
rsi가 35분 정도 횡보하다가 "35분 내 rsi 최고값" 혹은 " 35분 내 rsi 최저값'을 이탈할 때, 진입을 고민해 보기 위해, 필자가 임의로 정의해보았다.
228줄~236줄 관련해서 활용하는 방법은? 35분전~2분전 rsi의 최고값 혹은 최저값을 구하고, 1분전의 rsi가 이탈할 때, long/short 진입을 생각해보고 있다.
※ 알고리즘을 적용은 항상 신중하게 접근하자. 하나의 아이디어 차원이다.
256줄 : 37줄에서 키움증권에 요청할 데이터를, 256줄에서 입력(NQZ23, 1)해 준다.
4. 전체코드
아래 < 접은글 >의 코드를 실행하려면, 시세조회 이용료($185)를 지불하여야 하며, 143줄에서 사용자의 계좌번호 10자리를 넣어주어야 한다.
import sys
from PyQt5.QAxContainer import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class btl_system():
def __init__(self):
self.kiwoom = QAxWidget("KFOPENAPI.KFOpenAPICtrl.1")
print("로그인 시작!")
self.kiwoom.dynamicCall("CommConnect(1)") # CommConnect() : 괄호 안에 자동(1)을 넣는다.
self.kiwoom.OnEventConnect.connect(self.login_Connect)
self.kiwoom.OnReceiveTrData.connect(self.trdata_get)
self.login_event_loop = QEventLoop()
self.login_event_loop.exec_()
self.kiwoom.dynamicCall("GetCommonFunc(QString, QString)", "ShowAccountWindow", "") # 계좌번호 입력창을 띄우는 내부함수
##### rsi의 현재 1분 데이터의 au/ad 구하기 #####
self.price_rsi_total = [] # 158~163줄 : 1분봉들의 종가데이터를 담는 리스트 모음
self.close_price_rsi_change = [] # 164~167줄 : 기준값(au/ad)를 세팅하기 위해, 최근 15개 값을 담는 리스트
self.close_price_third = [] # 168~179줄 : 기준값 15개의 각 종가들의 차이 모음 리스트
self.early_au1 = [] # 175~179줄 : 음수를 0으로 표시
self.early_ad1 = [] # 175~179줄 : 양수를 0으로 표시
self.rsi_total_list = [] # 214~235줄 : 각각의 rsi를 담는 리스트 모음
def login_Connect(self, err_code):
if err_code == 0:
print('로그인 성공했습니다!')
else:
print('로그인 실패했습니다!')
self.login_event_loop.exit()
########## 키움서버에 TR 요청하는 함수 모음 ##########
def rq_data_opc10002(self, stock_code_num, time_unit): # 1분봉 받기
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", stock_code_num)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "시간단위", time_unit)
self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opc_10002", "opc10002", "", "1002")
self.tr_event_loop = QEventLoop()
self.tr_event_loop.exec_()
def rq_data_opw30009(self, deposit_num1, password_2, password_enter3): # 주문가능금액, 실현손익, 미실현손익
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opw_30009", "opw30009", "", "3009")
self.tr_event_loop = QEventLoop()
self.tr_event_loop.exec_()
def rq_data_opw30011(self, deposit_num1, password_2, password_enter3, futures_code4, sell_buy_gubunm5, order_type6, order_price7): # 주문가능수량
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", futures_code4)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "매도수구분", sell_buy_gubunm5)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "해외주문유형", order_type6)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "주문표시가격", order_price7) # "시장가"로 주문할 경우, 함수 실행시 빈칸("")으로 요청
self.kiwoom.dynamicCall("CommRqData(QString, QString, QString,QString)", "opw_30011", "opw30011", "", "3011")
self.tr_event_loop = QEventLoop()
self.tr_event_loop.exec_()
def rq_data_opw30012(self, deposit_num1, password_2, password_enter3): # 계좌 현재가, 매도수구분, 보유수량, 매입단가, 계좌현재가, 평가손익
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "계좌번호", deposit_num1)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호", password_2)
self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "비밀번호입력매체", password_enter3)
self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opw_30012", "opw30012", "", "3012")
self.tr_event_loop = QEventLoop()
self.tr_event_loop.exec_()
########## OnReceiveTrData을 통해 수신받은 데이터 함수 ##########
def trdata_get(self, scrno, rqname, trcode, recordname, prenext):
if rqname == "opc_10002":
getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString,QString)", trcode, recordname)
self.getrepeatcnt = getrepeatcnt
self.tr_event_loop.exit()
btl.rsi_searching()
elif rqname == "opw_30009": # 주문가능금액, 실현손익, 미실현손익
orderable_money = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "주문가능금액").strip())
withdrawal_money = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "인출가능금액").strip())
realized_pl = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "선물청산손익").strip())
unrealized_pl = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30009", "opw30009", 0, "선물평가손익").strip())
self.orderable_money = orderable_money / 100
self.withdrawal_money = withdrawal_money / 100
self.realized_pl = realized_pl / 100
self.unrealized_pl = unrealized_pl / 100
print(self.orderable_money)
self.tr_event_loop.exit()
elif rqname == "opw_30011": # 주문가능수량
orderable_qty = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "주문가능수량").strip())
liquidable_qty = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "청산가능수량").strip())
orderable_money_30011 = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "주문가능금액").strip())
currency_code = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30011", "opw30011", 0, "통화코드").strip()
self.orderable_qty = orderable_qty
self.liquidable_qty = liquidable_qty
self.orderable_money_30011 = orderable_money_30011 / 100
self.currency_code = currency_code
self.tr_event_loop.exit()
elif rqname == "opw_30012": # 계좌 현재가, 매도수구분, 보유수량, 매입단가, 계좌현재가, 평가손익
stock_code_d = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "종목코드").strip()
sell_buy_gubun_d = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "매도수구분").strip()
my_qty_d = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "수량").strip())
my_price_d = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "매입가격").strip())
current_price_d = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "현재가격").strip()))
estimate_pl_d = int(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opw_30012", "opw30012", 0, "평가손익").strip())
self.stock_code_d = stock_code_d
self.sell_buy_gubun_d = sell_buy_gubun_d
self.my_qty_d = my_qty_d
self.my_price_d_30012 = my_price_d
self.current_price_d_30012 = current_price_d
self.estimate_pl_d_30012 = estimate_pl_d / 100
self.tr_event_loop.exit()
########## 주문요청하는 함수 (키움 OPENApi-w에서 제공) ##########
def sendorder_func(self, rqname, scr, acc, ordertype, futures_code, qty, futures_price, s_stop, hogagb, orgno):
self.kiwoom.dynamicCall("SendOrder(Qstring, Qstring, Qstring, int, Qstring, int, Qstring, Qstring, Qstring, Qstring)",
[rqname, scr, acc, ordertype, futures_code, qty, futures_price, s_stop, hogagb, orgno])
"""
1) BSTR sRQName,
2) BSTR sScreenNo,
3) BSTR sAccNo,
4) LONG nOrderType, *주문유형(1: 신규매도, 2: 신규매수, 3: 매도취소, 4: 매수취소, 5: 매도정정, 6: 매수정정)
5) BSTR sCode,
6) LONG nQty,
7) BSTR sPrice,
8) BSTR sStop, * stop 단가
9) BSTR sHogaGb, * 거래구분(1: 시장가, 2: 지정가, 3: STOP, 4: STOP LIMIT)
10) BSTR sOrgOrderNo)
*(예시) ## openApi.SendOrder("RQ_1", "1000", "5077000072", 1, "6AZ20", / 1, "0.7900", "0", "2", ""); // 지정가 매도
openApi.SendOrder("RQ_1", "1000", "5077000072", 1, "6AZ20", / 1, "0", "0", "1", ""); // 시장가 매도
## openApi.SendOrder("RQ_1", "1000", "5077000072", 1, "6AZ20", / 1, "0", "0.7900", "3", ""); // STOP 매도
## openApi.SendOrder("RQ_1", "1000", "5077000072", 1, "6AZ20", / 1, "0.7850", "0.8000", "4", ""); // STOP LIMIT 매도
## openApi.SendOrder("RQ_1", "1000", "5077000072", 5, "6AZ20", / 1, "0.7850", "0", "2", "500060"); // 정정 매도
## openApi.SendOrder("RQ_1", "1000", "5077000072", 3, "6AZ20", / 1, "0", "0", "2", "500060"); // 취소 매도
"""
def rsi_searching(self):
for i in range(self.getrepeatcnt): # 현재가 → 오래된 값으로 출력된다.
self.current_price_rsi = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "현재가").strip())
self.price_rsi_total.append(self.current_price_rsi)
self.price_rsi_total_final = self.price_rsi_total[::-1]
self.price_rsi_total = []
for i in range(0, 15):
current_price_recent = self.price_rsi_total_final[i]
self.close_price_rsi_change.append(current_price_recent)
for j in range(len(self.close_price_rsi_change) - 1):
self.close_price_second = self.close_price_rsi_change[j + 1] - self.close_price_rsi_change[j]
self.close_price_third.append(self.close_price_second)
self.close_price_rsi_change = []
print("68줄")
for i in self.close_price_third: # 음수를 0으로 표시
if i <= 0:
i = 0
self.early_au1.append(i)
else:
self.early_au1.append(i)
for j in self.close_price_third: # 양수를 0으로 표시
if j >= 0:
j = 0
self.early_ad1.append(j)
else:
self.early_ad1.append(j)
self.au7 = sum(self.early_au1) / len(self.early_au1)
self.ad7 = sum(self.early_ad1) / len(self.early_ad1)
self.ad7 = abs(self.ad7)
self.early_au1 = [] # 초기화
self.early_ad1 = [] # 초기화
del self.close_price_third[:] # 향후 사용하지 않을 분봉 리스트는 초기화(삭제)하여 메모리 효율화 추진
for k in range(1, self.getrepeatcnt-1):
curren_price_rsi_1 = self.price_rsi_total_final[k]
curren_price_rsi_2 = self.price_rsi_total_final[k+1]
current_price_rsi_3 = curren_price_rsi_2 - curren_price_rsi_1
if current_price_rsi_3 >= 0:
positive_price = current_price_rsi_3
negative_price = 0
else:
positive_price = 0
negative_price = abs(current_price_rsi_3)
self.au8 = (self.au7 * 13 + positive_price) / 14
self.ad8 = (self.ad7 * 13 + negative_price) / 14
rs2 = self.au8 / self.ad8
self.rsi_second = rs2 / (1 + rs2) * 100
self.current_rsi = round(self.rsi_second, 2)
self.rsi_total_list.append(self.current_rsi)
self.au7 = self.au8
self.ad7 = self.ad8
self.price_rsi_total_final = []
self.rsi_total_list = self.rsi_total_list[::-1]
self.current_rsi_0 = self.rsi_total_list[0] # 현재시간
self.current_rsi_1 = self.rsi_total_list[1] # 1분전
print(self.current_rsi_0)
print(self.current_rsi_1)
self.rsi_total_list_2_35_minute = [] # 2분전 ~ 35분전
for i in range(2, 36):
self.rsi_total_list_2_35_minute.append(self.rsi_total_list[i])
self.rsi_max_2_35_minute_point = max(self.rsi_total_list_2_35_minute)
self.rsi_max_2_35_minute_position = self.rsi_total_list_2_35_minute.index(self.rsi_max_2_35_minute_point)
self.rsi_min_2_35_minute_point = min(self.rsi_total_list_2_35_minute)
self.rsi_min_2_35_minute_position = self.rsi_total_list_2_35_minute.index(self.rsi_min_2_35_minute_point)
print("####################################")
print(self.rsi_total_list_2_35_minute)
print(len(self.rsi_total_list_2_35_minute))
print(self.rsi_total_list[2])
print(self.rsi_total_list[35])
self.rsi_total_list_2_35_minute = []
self.rsi_total_list = []
print(self.rsi_max_2_35_minute_point)
print(self.rsi_max_2_35_minute_position)
print(self.rsi_min_2_35_minute_point)
print(self.rsi_min_2_35_minute_position)
if __name__ == "__main__":
app = QApplication(sys.argv)
btl = btl_system()
btl.rq_data_opc10002("NQZ23", "1")
app.exec_()
5. 마치며
1분봉의 rsi 구하는 방법에 대해 알아보았다. 그간 코드에서 가독성을 높이는 방향(171줄~183줄, 양수 및 음수를 구분하여 리스트에 담는 것)으로 글을 작성하다보니, 글이 조금 길어진 면이 있다.
또한, 필자가 생각하는 알고리즘(35분~2분의 rsi최고값 및 최저값에 대한 1분전 봉의 rsi 값 이탈)에 대해서도 꾸준히 고민해 봐야 할 것 같다.
보조지표 중 rsi 외에 새로운 내용은 없을 것으로 생각된다. 다음 글에서는 macd, 볼린저, 스토캐스틱에 대해 간단히 설명할 예정이다.
'2. 해외선물 > 2-1. 해외선물 자동매매 연구' 카테고리의 다른 글
(키움증권 해외선물 자동매매 파이썬) 18. 진입/청산 시도횟수 체크하기 (2) | 2023.10.25 |
---|---|
(키움증권 해외선물 자동매매 파이썬) 17. 시간별 다른 코드 적용하기 (3) | 2023.10.24 |
(키움증권 해외선물 자동매매 파이썬) 16. 현재시간 출력하기 (5) | 2023.10.23 |
(키움증권 해외선물 자동매매 파이썬) 15. 보조지표 구하기 (볼린저밴드, MACD, 스토캐스틱) (0) | 2023.10.22 |
(키움증권 해외선물 자동매매 파이썬) 13. 주문하기(SendOrder) (13) | 2023.10.06 |
(키움증권 해외선물 자동매매 파이썬) 12. 주문가능수량 조회 (opw30011) (4) | 2023.10.05 |
(키움증권 해외선물 자동매매 파이썬) 11. 주문가능금액 조회 (opw30009) (4) | 2023.09.30 |
(키움증권 해외선물 자동매매 파이썬) 10. 매도수구분, 진입가격, 청산가격, 평가손익 알아보기(opw30012) (0) | 2023.09.29 |