RLTrader의 에이전트 모듈 개발

2020-03-31 • rltraderstock, 주식투자, reinforcement learning, rl, 강화학습, agent, 에이전트 • 10 min read

에이전트 모듈에 포함된 에이전트 클래스(Agent)의 속성과 함수를 살펴보고 파이썬으로 구현한 소스 코드를 상세히 확인합니다.

에이전트 모듈의 주요 속성과 함수

에이전트 모듈(agent.py)은 투자 행동을 수행하고 투자금과 보유 주식을 관리하기 위한 에이전트 클래스(Agent)를 가집니다. Agent 클래스의 주요 속성과 함수는 다음과 같습니다.

속성

  • initial_balance: 초기 투자금
  • balance: 현금 잔고
  • num_stocks: 보유 주식 수
  • portfolio_value: 포트폴리오 가치(투자금 잔고 + 주식 현재가 * 보유 주식 수)
  • num_buy: 매수 횟수
  • num_sell: 매도 횟수
  • num_hold: 관망 횟수
  • immediate_reward: 즉시 보상
  • profitloss: 현재 손익
  • base_profitloss: 직전 지연 보상 이후 손익
  • exploration_base: 탐험 행동 결정 기준

함수

  • reset(): 에이전트의 상태를 초기화
  • set_balance(): 초기 자본금을 설정
  • get_states(): 에이전트 상태를 획득
  • decide_action(): 탐험 또는 정책 신경망에 의한 행동 결정
  • validate_action(): 행동의 유효성 판단
  • decide_trading_unit(): 매수 또는 매도할 주식 수 결정
  • act(): 행동 수행

에이전트 모듈은 환경 모듈보다는 규모 면에서 더 큽니다. 그래서 소스 코드를 한번에 보여주는 것이 효과적이지 않아서 상수 선언 부분 및 함수 단위로 설명합니다.

코드 조각 1: 에이전트 클래스의 상수 선언

다음은 에이전트 클래스의 상수 선언부의 소스 코드입니다.

Agent 클래스: 상수 선언 부분

import numpy as np
import utils

class Agent:
    # 에이전트 상태가 구성하는 값 개수
    STATE_DIM = 2  # 주식 보유 비율, 포트폴리오 가치 비율

    # 매매 수수료 및 세금
    TRADING_CHARGE = 0.00015  # 거래 수수료 (일반적으로 0.015%)
    TRADING_TAX = 0.0025  # 거래세 (실제 0.25%)
    # TRADING_CHARGE = 0  # 거래 수수료 미적용
    # TRADING_TAX = 0  # 거래세 미적용

    # 행동
    ACTION_BUY = 0  # 매수
    ACTION_SELL = 1  # 매도
    ACTION_HOLD = 2  # 홀딩
    # 인공 신경망에서 확률을 구할 행동들
    ACTIONS = [ACTION_BUY, ACTION_SELL]
    NUM_ACTIONS = len(ACTIONS)  # 인공 신경망에서 고려할 출력값의 개수

에이전트 모듈은 NumPy를 사용하므로 우선 임포트합니다. numpy 모듈을 np로 줄여서 정의했습니다.

파이썬 팁: import는 다른 패키지, 모듈, 클래스, 함수에 접근할 수 있게 연결하는 명령어입니다. as 키워드로 임포트한 패키지, 모듈, 클래스, 함수 등을 다른 이름으로 지정할 수도 있습니다. 예를 들어, import numpy as np는 numpy 모듈을 np로 사용하겠다는 의미입니다.

Agent 클래스는 여러 상수를 사용합니다. 우선 STATE_DIM은 에이전트 상태의 차원입니다. RLTrader에서의 에이전트 상태는 주식 보유 비율과 포트폴리오 가치 비율로 2개의 값을 가지므로 2차원입니다. 에이전트의 상태에 대해서는 뒤에서 더 설명합니다.

에이전트는 매수와 매도를 수행하는 주체이기 때문에 매매 수수료 및 세금을 상수로 가집니다. TRADING_CHARGE는 매수 및 매도 수수료를, TRADING_TAX는 거래세를 의미합니다.

사실 실제 주식투자에서는 거래 수수료와 거래세는 수익성을 좌우하는 큰 요소입니다. 이 책에서의 주식투자 시뮬레이션은 일봉 차트로 매일 주식을 매매하므로 거래 횟수가 아주 많습니다. 이렇게 거래를 많이 하는 것은 현실적으로 좋은 방법이 아닙니다.

이 책은 주식투자 자체에 관점을 맞춘 것이 아니라 개인이 딥러닝을 주식투자에 (시험적으로) 적용해 볼 수 있게 필요한 프로그래밍 지식을 전달하는 데 집중하고 있습니다. 따라서 이 책에서는 거래 수수료와 거래세를 고려하지 않습니다. 실전 투자에 RLTrader를 적용하고자 한다면 거래 횟수를 주 1회, 월 1회, 연 1회 등으로 줄이고 일봉 차트뿐만 아니라 다양한 지표를 활용해야 할 것입니다. 일반적으로 거래 수수료는 0.015%, 거래세는 0.25%로 생각하면 됩니다. 거래 수수료와 거래세를 고려하고 싶지 않다면 TRADING_CHARGETRADING_TAX를 모두 0으로 정하면 됩니다.

에이전트가 할 수 있는 행동은 매수, 매도, 관망입니다. 에이전트는 이 행동들에 특정 값을 부여해 상수로 가집니다. ACTION_BUY는 매수 행동을 의미하며 0을 값으로 가지고, ACTION_SELL은 매도 행동이고 1을 할당하고, ACTION_HOLD는 매수도 매도도 하지 않는 관망 행동으로 2의 값을 할당합니다. 이 행동들 중에서 정책 신경망이 확률을 계산할 행동들을 ACTIONS 리스트에 저장합니다. RLTrader는 매수와 매도에 대한 확률만 계산하고 매수와 매도 중에서 결정한 행동을 할 수 없을 때만 관망 행동을 합니다.

코드 조각 2: 에이전트 클래스의 생성자

이제 에이전트 클래스의 생성자 함수를 살펴보겠습니다.

Agent 클래스: 생성자

    def __init__(
        self, environment, min_trading_unit=1, max_trading_unit=2, 
        delayed_reward_threshold=.05):
        # Environment 객체
        # 현재 주식 가격을 가져오기 위해 환경 참조
        self.environment = environment

        # 최소 매매 단위, 최대 매매 단위, 지연보상 임계치
        self.min_trading_unit = min_trading_unit  # 최소 단일 거래 단위
        self.max_trading_unit = max_trading_unit  # 최대 단일 거래 단위
        # 지연보상 임계치
        self.delayed_reward_threshold = delayed_reward_threshold

        # Agent 클래스의 속성
        self.initial_balance = 0  # 초기 자본금
        self.balance = 0  # 현재 현금 잔고
        self.num_stocks = 0  # 보유 주식 수
        # PV = balance + num_stocks * {현재 주식 가격}
        self.portfolio_value = 0 
        self.base_portfolio_value = 0  # 직전 학습 시점의 PV
        self.num_buy = 0  # 매수 횟수
        self.num_sell = 0  # 매도 횟수
        self.num_hold = 0  # 홀딩 횟수
        self.immediate_reward = 0  # 즉시 보상
        self.profitloss = 0  # 현재 손익
        self.base_profitloss = 0  # 직전 지연 보상 이후 손익
        self.exploration_base = 0  # 탐험 행동 결정 기준

        # Agent 클래스의 상태
        self.ratio_hold = 0  # 주식 보유 비율
        self.ratio_portfolio_value = 0  # 포트폴리오 가치 비율

생성자의 매개변수로 environment, min_trading_unit, max_trading_unit, delayed_reward_threshold를 받습니다. environmentEnvironment 클래스의 객체입니다. min_trading_unit은 최소한의 매매 단위고 max_trading_unit은 최대의 매매 단위입니다. max_trading_unit을 크게 잡으면 결정한 행동에 대한 확신이 높을 때 더 많이 매수 또는 매도할 수 있게 설계했습니다. delayed_reward_threshold는 지연 보상 임계치로, 손익률이 이 값을 넘으면 지연 보상이 발생합니다.

initial_balance는 초기 자본금으로, 투자 시작 시점의 보유 현금입니다. balance는 현재의 현금 잔고입니다. num_stocks는 현재의 보유 주식 수입니다. portfolio_value는 포트폴리오 가치로, 보유 현금과 보유 주식 수에 현재 주가를 곱해 더한 값입니다. base_portfolio_value는 목표 수익률 또는 기준 손실률을 달성하기 전의 과거 포트폴리오 가치로 현재 포트폴리오 가치가 증가했는지 또는 감소했는지를 비교할 기준이 됩니다. exploration_base는 탐험의 기준 확률로, 탐험을 하더라도 매수를 기조로 할지, 매도를 기조로 할지를 정하는 것입니다. 뒤에서 이 변수가 어떻게 쓰이는지 더 자세히 알아보겠습니다.

num_buy, num_sell, num_hold는 각각 에이전트가 행한 매수, 매도, 관망 횟수입니다. immediate_reward는 에이전트가 가장 최근 행한 행동에 대한 즉시 보상 값입니다. 신경망에 입력으로 들어가는 샘플에 에이전트의 상태도 포함됩니다. 여기서 에이전트 상태는 주식 보유 비율을 담는 ratio_hold와 포트폴리오 가치 비율을 담는 ratio_portfolio_value를 가집니다.

코드 조각 3: 에이전트 클래스의 함수

에이전트 클래스의 속성값 획득(Get) 함수와 설정(Set) 함수를 살펴보겠습니다.

Agent 클래스: Get/Set 함수

    def reset(self):
        self.balance = self.initial_balance
        self.num_stocks = 0
        self.portfolio_value = self.initial_balance
        self.base_portfolio_value = self.initial_balance
        self.num_buy = 0
        self.num_sell = 0
        self.num_hold = 0
        self.immediate_reward = 0
        self.ratio_hold = 0
        self.ratio_portfolio_value = 0

    def reset_exploration(self):
        self.exploration_base = 0.5 + np.random.rand() / 2

    def set_balance(self, balance):
        self.initial_balance = balance

    def get_states(self):
        self.ratio_hold = self.num_stocks / int(
            self.portfolio_value / self.environment.get_price())
        self.ratio_portfolio_value = (
            self.portfolio_value / self.base_portfolio_value
        )
        return (
            self.ratio_hold,
            self.ratio_portfolio_value
        )

reset() 함수는 Agent 클래스의 속성들을 초기화합니다. 학습 단계에서 한 에포크마다 에이전트의 상태를 초기화해야 합니다. reset_exploration() 함수는 탐험의 기준이 되는 exploration_base를 새로 정하는 함수입니다. 매수 탐험을 선호하기 위해서 50% 매수 탐험 확률을 미리 부여했습니다. set_balance() 함수는 에이전트의 초기 자본금을 설정합니다. get_states() 함수는 에이전트의 상태를 반환합니다. 상태는 다음 두 값으로 구성됩니다.

주식 보유 비율 = 보유 주식 수/(포트폴리오 가치/현재 주가)

주식 보유 비율은 현재 상태에서 가장 많이 가질 수 있는 주식 수 대비 현재 보유한 주식의 비율입니다. 이 값이 0이면 주식을 하나도 보유하지 않은 것이고 0.5이면 최대 가질 수 있는 주식 대비 절반의 주식을 보유하고 있는 것이며 1이면 최대로 주식을 보유하고 있는 것입니다. 아무래도 주식 수가 너무 적으면 매수의 관점에서 투자에 임하고 주식 수가 너무 많으면 매도의 관점에서 투자에 임하게 됩니다. 즉, 보유 주식 수를 투자 행동 결정에 영향을 주기 위해 정책 신경망의 입력에 포함합니다.

포트폴리오 가치 비율 = 포트폴리오 가치/기준 포트폴리오 가치

포트폴리오 가치 비율은 기준 포트폴리오 가치 대비 현재 포트폴리오 가치의 비율입니다. 기준 포트폴리오 가치는 직전에 목표 수익 또는 손익률을 달성했을 때의 포트폴리오 가치입니다. 이 값은 현재 수익이 발생했는지 손실이 발생했는지를 판단할 수 있는 값입니다. 포트폴리오 가치 비율이 0에 가까우면 손실이 큰 것이고 1보다 크면 수익이 발생했다는 뜻입니다. 수익률이 목표 수익률에 가까우면 매도의 관점에서 투자하고는 합니다. 수익률이 투자 행동 결정에 영향을 줄 수 있기 때문에 이 값을 에이전트의 상태로 정하고 정책 신경망의 입력값으로 포함합니다.

파이썬 팁: int()는 숫자를 정수로 캐스팅(형식 변환)하는 파이썬 내장 함수입니다. 예를 들어, int(0.5)는 0을, int(1.3)은 1을 반환합니다.

파이썬 팁: 파이썬에서 (a, b, …)는 튜플(tuple)을 의미합니다. 튜플은 리스트 [a, b, …]와 비슷합니다. 차이점은 튜플은 요소를 추가, 변경, 삭제하는 처리가 불가능하고 리스트는 가능하다는 것입니다. 튜플이 메모리를 적게 사용하기 때문에 요소를 수정할 필요가 없으면 튜플을 사용하는 것이 효율적입니다.

다음은 에이전트가 행동을 결정하고 결정한 행동의 유효성을 검사하는 함수를 보여줍니다.

Agent 클래스: 행동 결정 검사 함수

    def decide_action(self, pred_value, pred_policy, epsilon):
        confidence = 0.

        pred = pred_policy
        if pred is None:
            pred = pred_value

        if pred is None:
            # 예측 값이 없을 경우 탐험
            epsilon = 1
        else:
            # 값이 모두 같은 경우 탐험
            maxpred = np.max(pred)
            if (pred == maxpred).all():
                epsilon = 1

        # 탐험 결정
        if np.random.rand() < epsilon:
            exploration = True
            if np.random.rand() < self.exploration_base:
                action = self.ACTION_BUY
            else:
                action = np.random.randint(self.NUM_ACTIONS - 1) + 1
        else:
            exploration = False
            action = np.argmax(pred)

        confidence = .5
        if pred_policy is not None:
            confidence = pred[action]
        elif pred_value is not None:
            confidence = utils.sigmoid(pred[action])

        return action, confidence, exploration

decide_action()은 입력으로 들어온 엡실론(Epsilon)의 확률로 무작위로 행동을 결정하고 그렇지 않은 경우에 신경망을 통해 행동을 결정합니다.

0에서 1 사이의 랜덤 값을 생성하고 이 값이 엡실론보다 작으면 무작위로 행동을 결정합니다. 탐험의 기조로 작용하는 exploration_base는 에포크마다 새로 결정됩니다. exploration_base가 1에 가까우면 탐험할 때 매수를 더 많이 선택할 것입니다. 반대로 exploration_base가 0에 가까우면 매도 탐험을 더 많이 할 것입니다. 이렇게 기조를 정하는 이유는 하나의 에포크에서 [매수, 매도, 매수, 매도]와 같은 효과적이지 않은 탐험을 하지 않게 하기 위해서입니다.

여기서 NUM_ACTIONS는 2의 값을 가집니다. 그러므로 랜덤으로 행동을 결정하면 0(매수) 또는 1(매도)의 값을 결정하는 것입니다.

탐험을 하지 않는 경우 신경망을 통해 행동을 결정합니다. 정책 신경망의 출력인 pred_policy가 있으면 pred_policy로 행동을 결정하고, 없으면 pred_value로 행동을 결정합니다. DQNLearner의 경우 pred_policyNone이므로 pred_value로 행동을 결정합니다. 신경망 클래스의 함수는 5.5절에서 상세하게 다룹니다.

파이썬 팁: NumPy의 random 모듈은 랜덤 값 생성을 위한 rand() 함수를 제공합니다. 이 함수는 0에서 1 사이의 값을 생성해 반환합니다. randint(low, high=None) 함수는 high를 넣지 않은 경우 0에서 low 사이의 정수를 랜덤으로 생성하고 high를 넣은 경우 low에서 high 사이의 정수를 생성합니다.

파이썬 팁: NumPy의 argmax(array) 함수는 입력으로 들어온 array에서 가장 큰 값의 위치를 반환합니다. 예를 들어, array[3, 5, 7, 0, -3]이면 가장 큰 값은 7이므로 그 위치인 2를 반환합니다. 파이썬에서 위치(index)는 0부터 시작합니다.

다음은 결정한 행동의 유효성을 검사하는 함수입니다.

Agent 클래스: 유효성 검사 함수

    def validate_action(self, action):
        if action == Agent.ACTION_BUY:
            # 적어도 1주를 살 수 있는지 확인
            if self.balance < self.environment.get_price() * (
                1 + self.TRADING_CHARGE) * self.min_trading_unit:
                return False
        elif action == Agent.ACTION_SELL:
            # 주식 잔고가 있는지 확인 
            if self.num_stocks <= 0:
                return False
        return True

RLTrader에서는 신용 매수나 공매도는 고려하지 않습니다. 신용 매수는 잔금이 부족하더라도 돈을 빌려서 매수를 하는 것이고 공매도는 주식을 보유하고 있지 않더라도 미리 매도하고 나중에서 주식을 사서 갚는 방식의 거래입니다.

그러므로 이렇게 결정한 행동은 특정 상황에서는 수행할 수 없을 수도 있습니다. 예를 들어, 매수를 결정했는데 잔금이 1주 매수하기에도 부족한 경우나 매도를 결정했는데 보유하고 있는 주식이 하나도 없는 경우에 결정한 행동을 수행할 수 없습니다. 그래서 결정한 행동을 수행할 수 있는지를 확인하기 위해서 validate_action() 함수를 사용합니다.

매수 결정에 대해서 적어도 1주를 살 수 있는 잔금이 있는지 확인합니다. 이때 거래 수수료까지 고려합니다. 매도 결정에 대해서 주식 잔고가 있는지 확인합니다. num_stocks 변수에 현재 보유한 주식 수가 저장됩니다.

다음 코드조각에서 결정한 행동의 신뢰(confidence)에 따라서 매수 또는 매도의 단위를 조정하는 함수를 살펴봅니다.

Agent 클래스: 매수/매도 단위 결정 함수

    def decide_trading_unit(self, confidence):
        if np.isnan(confidence):
            return self.min_trading_unit
        added_traiding = max(min(
            int(confidence * (self`.max_trading_uni`t - 
                self.min_trading_unit)),
            self.max_trading_unit-self.min_trading_unit
        ), 0)
        return self.min_trading_unit + added_traiding

decide_trading_unit() 함수는 정책 신경망이 결정한 행동의 신뢰가 높을수록 매수 또는 매도하는 단위를 크게 정해줍니다. 높은 신뢰로 매수를 결정했으면 그에 맞게 더 많은 주식을 매수하고 높은 신뢰로 매도를 결정했으면 더 많은 보유 주식을 매도하는 것입니다.

다음으로 투자 행동을 수행하는 함수의 초반부를 살펴봅니다.

Agent 클래스: 투자 행동 수행 함수 (1)

    def act(self, action, confidence):
        if not self.validate_action(action):
            action = Agent.ACTION_HOLD

        # 환경에서 현재 가격 얻기
        curr_price = self.environment.get_price()

        # 즉시 보상 초기화
        self.immediate_reward = 0

act() 함수는 에이전트가 결정한 행동을 수행합니다. 인자로는 action과 confidence를 받습니다. action은 탐험 또는 정책 신경망을 통해 결정한 행동으로 매수와 매도를 의미하는 0 또는 1의 값입니다. confidence는 정책 신경망을 통해 결정한 경우 결정한 행동에 대한 소프트맥스 확률 값입니다.

먼저 이 행동을 할 수 있는지 확인하고 할 수 없는 경우 아무 행동도 하지 않게 관망(hold)합니다. 그리고 환경 객체에서 현재의 주가를 받아옵니다. 이 가격은 매수 금액, 매도 금액, 포트폴리오 가치를 계산할 때 사용됩니다. 즉시 보상은 에이전트가 행동할 때마다 결정되기 때문에 초기화합니다.

act() 함수의 매수 행동 수행 부분을 이어서 살펴봅니다.

Agent 클래스: 투자 행동 수행 함수 (2)

        # 매수
        if action == Agent.ACTION_BUY:
            # 매수할 단위를 판단
            trading_unit = self.decide_trading_unit(confidence)
            balance = (
                self.balance - curr_price * (1 + self.TRADING_CHARGE) \
                    * trading_unit
            )
            # 보유 현금이 모자랄 경우 보유 현금으로 가능한 만큼 최대한 매수
            if balance < 0:
                trading_unit = max(
                    min(
                        int(self.balance / (
                            curr_price * (1 + self.TRADING_CHARGE))),
                        self.max_trading_unit
                    ),
                    self.min_trading_unit
                )
            # 수수료를 적용해 총 매수 금액 산정
            invest_amount = curr_price * (1 + self.TRADING_CHARGE) \
                * trading_unit
            if invest_amount > 0:
                self.balance -= invest_amount  # 보유 현금을 갱신
                self.num_stocks += trading_unit  # 보유 주식 수를 갱신
                self.num_buy += 1  # 매수 횟수 증가

먼저 수행할 행동이 매수인지를 확인하고 매수 단위(살 주식 수)를 정합니다. 그리고 매수 후의 잔금을 확인합니다. 신용 매수를 고려하지 않으므로 매수 후 잔금이 0보다 적을 수는 없습니다.

결정한 매수 단위가 최대 단일 거래 단위를 넘어가면 최대 단일 거래 단위로 제한하고 최소 거래 단위보다 최소한 1주를 매수합니다.

파이썬 팁: 파이썬의 내장 함수인 min(a, b)max(a, b) 함수에 대해 알아보겠습니다. min(a, b)ab 중에서 작은 값을 반환합니다. 반대로 max(a, b)ab 중에서 큰 값을 반환합니다. 예를 들어, min(5, 10)은 5를, max(-5, -10)은 -5를 반환합니다.

매수할 단위에 수수료를 적용해 총 투자 금액을 계산합니다. 그리고 이 금액을 현재 잔금에서 빼고 주식 보유 수를 투자 단위만큼 늘려 줍니다. 그리고 통계 정보인 num_buy를 1만큼 증가시킵니다.

이어서 매도와 관망에 대한 부분을 살펴보겠습니다.

Agent 클래스: 투자 행동 수행 함수 (3)

        # 매도
        elif action == Agent.ACTION_SELL:
            # 매도할 단위를 판단
            trading_unit = self.decide_trading_unit(confidence)
            # 보유 주식이 모자랄 경우 가능한 만큼 최대한 매도
            trading_unit = min(trading_unit, self.num_stocks)
            # 매도
            invest_amount = curr_price * (
                1 - (self.TRADING_TAX + self.TRADING_CHARGE)) \
                    * trading_unit
            if invest_amount > 0:
                self.num_stocks -= trading_unit  # 보유 주식 수를 갱신
                self.balance += invest_amount  # 보유 현금을 갱신
                self.num_sell += 1  # 매도 횟수 증가

        # 홀딩
        elif action == Agent.ACTION_HOLD:
            self.num_hold += 1  # 홀딩 횟수 증가

수행한 행동이 매도인지 확인하고 매수 때와 마찬가지로 투자 단위를 판단합니다. 여기서는 투자 단위가 매도할 주식 수가 됩니다. 현재 가지고 있는 주식 수보다 결정한 매도 단위가 많으면 안 되기 때문에 현재 보유 주식 수를 최대 매도 단위로 제한합니다.

다음으로 매도 금액을 계산합니다. 이때 정해준 매도 수수료와 거래세를 모두 고려해 계산합니다. 그리고 보유 주식 수를 매도한 만큼 빼고 매도해 현금화한 금액을 잔고에 더해줍니다. 통계를 위한 변수인 num_sell을 1만큼 증가시킵니다.

관망(hold)은 결정한 매수(buy)나 매도(sell) 행동을 수행할 수 없는 경우에 적용됩니다. 관망은 아무것도 하지 않는 것이기 때문에 보유 주식 수나 잔고에 영향을 미치지 않습니다. 대신 가격 변동은 있을 수 있으므로 포트폴리오 가치는 변동됩니다. 관망 횟수인 num_hold를 1만큼 증가시킵니다.

이어서 act() 함수의 후반부를 살펴봅니다.

Agent 클래스: 투자 행동 수행 함수 (4)

        # 포트폴리오 가치 갱신
        self.portfolio_value = self.balance + curr_price \
            * self.num_stocks
        self.profitloss = (
            (self.portfolio_value - self.initial_balance) \
                / self.initial_balance
        )

        # 즉시 보상 - 수익률
        self.immediate_reward = self.profitloss

        # 지연 보상 - 익절, 손절 기준
        delayed_reward = 0
        self.base_profitloss = (
            (self.portfolio_value - self.base_portfolio_value) \
                / self.base_portfolio_value
        )
        if self.base_profitloss > self.delayed_reward_threshold or \
            self.base_profitloss < -self.delayed_reward_threshold:
            # 목표 수익률 달성하여 기준 포트폴리오 가치 갱신
            # 또는 손실 기준치를 초과하여 기준 포트폴리오 가치 갱신
            self.base_portfolio_value = self.portfolio_value
            delayed_reward = self.immediate_reward
        else:
            delayed_reward = 0

        return self.immediate_reward, delayed_reward

이렇게 매수나 매도, 관망을 하면 포트폴리오 가치에 변동이 생깁니다. 포트폴리오 가치는 잔고, 주식 보유 수, 현재 주식 가격에 의해 결정됩니다. 기준 포트폴리오 가치에서 현재 포트폴리오 가치의 등락률을 계산합니다. 기준 포트폴리오 가치는 과거에 학습을 수행한 시점의 포트폴리오 가치를 의미한다고 했습니다.

즉시 보상은 기준 포트폴리오 가치 대비 현재 포트폴리오 가치의 비율로 정했습니다. 지연 보상은 즉시 보상이 지연 보상 임계치인 delayed_reward_threshold를 초과하는 경우 즉시 보상 값으로 정하고 그 외의 경우는 0으로 설정됩니다. RLTrader는 지연 보상이 0이 아닌 경우 학습을 수행합니다. 즉, 지연 보상 임계치를 초과하는 수익이 났으면 이전에 했던 행동들을 잘했다고 보고 긍정적으로(positive) 학습하고, 지연 보상 임계치를 초과하는 손실이 났으면 이전 행동들에 문제가 있다고 보고 부정적으로(negative) 학습할 것입니다.