스터디/Tech

[기술서적] 파이썬을 활용한 금융 분석 - 수학용 도구

_leezoee_ 2024. 3. 3. 15:47

 

 

파이썬을 활용한 금융분석 - 이브 힐피시 책 내용을 기반으로 colab, jupyter notebook 으로 직접 코드 구현해보며 공부한 내용이다.

 

 

 

파이썬을 통해 수학용 도구/테크닉 사용

 

 

1.근사법 : 금융 분야에서 가장 흔히 사용되는 수치 방법 중 하나인 회귀법, 보간법

2. 최적화 : 옵션 가격 계산 시 모형 캘리브레이션과 관련해 최적화 도구 필요

3. 적분 : 금융 파생상품의 가치 평가

4. 심볼릭 연산 : SymPy라는 연립방적식 계산 등이 가능한 심볼릭 연산 도구를 제공.

 

 

 

 

근사화

import numpy as np
from pylab import plt, mpl

plt.style.use('seaborn')
mpl.rcParams['font.family'] = 'serif'
%matplotlib inline

def f(x) : 
    return np.sin(x) + 0.5 * x #삼각함수와 선형 항의 합으로 구성

def create_plot(x, y, styles, labels, axlabels) : 
    plt.figure(figsize = (10,6))
    for i in range(len(x)):
        plt.plot(x[i], y[i], styles[i], label=labels[i])
        plt.xlabel(axlabels[0])
        plt.ylabel(axlabels[1])
    plt.legend(loc=0)
    
x = np.linspace(-2 * np.pi, 2*np.pi, 50) #플롯과 계산에 사용되는 x 값

create_plot([x], [f(x)], ['b'] , ['f(x)'] , ['x' , 'f(x)'])

 

예제 함수 플롯

 

 

 

* 회귀법 : 기저 함수 집합에서 기저 함수를 조합하기 위한 최적의 매개변수를 찾는 작업.

 

- 단항식 기저 함수

: Numpy는 이 경우 최적 매개변수를 결정하는 polyfit() 함수와 주어진 입력 값에 대해 근찻값을 계산하는 polyval()을 사용.

polyfit과 polyval 함수를 deg=1로 사용하면 선형회귀가 됨.

선형회귀를 쓰면 sin 부분은 설명 불가.

 

ployfit() : 다항식을 적합시키는 함수, 주어진 데이터에 대해 최적의 다항식을 찾아주는데 사용, 주어진 데이터 포인트들을 통해 최적의 선형 또는 곡선을 적합시키는데 사용.

ployval() : ployfit으로 찾은 다항식에 대해 값을 계산하는 함수, x값에 대한 y 값을 계

 

res = np.polyfit(x, f(x), deg=1, full=True) #선형회귀 단계

ry = np.polyval(res[0], x) #회귀모수를 사용해 계산한 값
create_plot([x,x] , [f(x), ry] , ['b' , 'r.'] , ['f(x)' , 'regression'] , ['x' , 'f(x)'])

 

선형회귀

 

#5차 단항식 기저함수로 사용

res = np.polyfit(x, f(x), deg=5)

ry = np.polyval(res, x)
create_plot([x,x] , [f(x), ry] , ['b' , 'r.'] , ['f(x)' , 'regression'] , ['x' , 'f(x)'])

 

5차 단항식 회귀 결과

 

 

#7차 단항식 기저함수로 활용

res = np.polyfit(x, f(x), 7)
ry = np.polyval(res, x)
create_plot([x,x] , [f(x), ry] , ['b' , 'r.'] , ['f(x)' , 'regression'] , ['x' , 'f(x)'])

np.mean((f(x) - ry) ** 2) #0.0017769134759517593 평균제곱오차 MSE 계산

7차 단항식 회귀 결과

 

 

 

근사화하고자 하는 함수에 대해 알고있는 지식을 사용해 더 적합한 기저 함수를 선택하면 회귀 결과를 향상시킬 수 있다.

함수에 sin 이 포함되어있기 때문에 기저 함수 집합에 사인 함수를 포함한다. 

가장 고차인 단항식을 사인 함수로 바꿔 회귀 결과를 도출한다.

 

np.linalg.lstsq() : NumPy 라이브러리에서 제공되는 최소 제곱 문제의 해를 구하는 함수로 주어진 데이터에 대해 최소 제곱을 사용해 선형 방정식을 해결.

matrix = np.zeros((3 + 1, len(x))) #기저 함숫값용 ndarray 객체(행렬), 상수부터 3차까지 기저 함수값
matrix[3, :] = x ** 3
matrix[2, :] = x ** 2
matrix[1, :] = x
matrix[0, :] = 1

matrix[3, :] = np.sin(x) #sin 함수 적용한 새 기저 함수
reg = np.linalg.lstsq(matrix.T, f(x), rcond=None)[0]

ry = np.dot(reg, matrix)

create_plot([x,x] , [f(x), ry], ['b', 'r.'], ['f(x)' , 'regression'] , ['x' , 'f(x)'])

np.allclose(f(x) , ry) #함수와 회귀 결과가 비슷한지 비교한다 (True, False 리턴)

 

사인 기저 함수 적용 회귀 결과

 

 

 

회귀법은 부정확한 측정으로 얻은 데이터에도 적용 가능하다.

독립변수와 측정값에 잡음이 있는 경우 회귀법은 어느 정도 잡음을 소거하는 특성을 가진다.

 

xn = np.linspace(-2 * np.pi, 2 * np.pi, 50) # 새로운 x 값
xn = xn + 0.15 * np.random.standard_normal(len(xn)) # x값에 잡음 추가
yn = f(xn) + 0.25 * np.random.standard_normal(len(xn)) #y값에 잡음 추가

reg = np.polyfit(xn, yn, 7)
ry = np.polyval(reg, xn)

create_plot([x,x] , [f(x), ry], ['b', 'r.'], ['f(x)' , 'regression'] , ['x' , 'f(x)'])

 

잡음이 있는 데이터 회귀 결과

 

 

회귀법은 정렬되지 않은 데이터에도 적용할 수 있다.

 

xu = np.random.rand(50) * 4 * np.pi - 2 * np.pi # x값 난수화
yu = f(xu)

reg = np.polyfit(xu, yu, 5)
ry = np.polyval(reg, xu)

create_plot([xu,xu] , [yu, ry], ['b.', 'ro'], ['f(x)' , 'regression'] , ['x' , 'f(x)'])

 

정렬되지 않은 데이터 회귀 결과

 

 

- 다차원 데이터

최소 자승 회귀법의 장점은 별다른 수정없이 다차원 데이터에도 쓸 수 있다는 점이다.

X, Y, Z에 들어간 독립변수와 종속변수 그리드에 기반하여 시각화를 진행한다.

 

def fm(p):
    x, y = p
    return np.sin(x) + 0.25 * x + np.sqrt(y) + 0.05 * y ** 2

x = np.linspace(0, 10, 20)
y = np.linspace(0, 10, 20)
X,Y = np.meshgrid(x, y) # 1차원 ndarray 객체에서 2차원 ndarray 객체 생성, (그리드)

Z = fm((X, Y))
x = X.flatten()
y = Y.flatten()
# flatten을 이용해 ndarray 객체를 1차원 ndarray로 만듦.

from mpl_toolkits.mplot3d import Axes3D # 3차원 플롯 기능

fig = plt.figure(figsize = (10, 6))
ax = fig.gca(projection = '3d')
surf = ax.plot_surface(X, Y, Z, rstride = 2, cstride = 2, 
                      cmap = 'coolwarm' , linewidth = 0.5,
                      antialiased = True)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x, y)')
fig.colorbar(surf, shrink=0.5, aspect=5)

두 개의 인수를 가진 함수

 

 

 

나은 회귀 결과를 위해 fm()에 대해 알고있는 정보를 사용해 np.sin() 함수와 np.sqrt() 함수를 포함하는 기저 함수 집합을 만들어 준다.

matrix = np.zeros((len(x) , 6 + 1))
matrix[:, 6] = np.sqrt(y) 
matrix[:, 5] = np.sin(x)
matrix[:, 4] = y ** 2 
matrix[:, 3] = x ** 2
matrix[:, 2] = y
matrix[:, 1] = x
matrix[:, 0] = 1

reg = np.linalg.lstsq(matrix, fm((x,y)), rcond = None)[0]

RZ = np.dot(matrix, reg).reshape((20,20)) #회귀분석 결과를 그리드 구조로 변환

fig = plt.figure(figsize = (10,6))
ax = fig.gca(projection = '3d')

surf1 = ax.plot_surface(X, Y, Z, rstride=2, cstride = 2,
                       cmap = mpl.cm.coolwarm, linewidth = 0.5, 
                       antialiased = True) #원래 함수 플롯
surf2 = ax.plot_wireframe(X, Y, RZ, rstride=2, cstride=2, label='regression') # 회귀분석 결과 플롯

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x,y)')
ax.legend()
fig.colorbar(surf, shrink=0.5, aspect=5)

두 개 인수를 가진 함수 회귀 결과

 

 

* 보간법 : x 차원의 정렬된 관측점이 주어졌을 때, 두 개 이웃하는 관측점 사이의 자료를 계산하는 보간 함수를 만드는 것.

주어진 데이터 포인트들로부터 빠진 값들을 예측하거나, 주어진 데이터를 부드럽게 연결하는 곡선을 생성하는데 활용.

(데이터의 빈 공간을 메우는 방법)

 

- 선형 스플라인 보간법 구현 : 데이터 포인트 사이를 선형으로 연결해 곡선을 생성하는 방법으로 데이터 포인트들을 연결하는 부드럽고 연속적인 곡선(spline) 사용. 데이터가 비교적 간단하고 부드러운 곡선이 필요한 경우 유용.

 

"""
spi.splrep 함수는 Scipy 서브패키지인 scipy.interpolate 에서 제공되는 함수로
spline보간법에 사용되는 B-스플라인 표현을 준비하는 함수.

tck = spi.splrep(x, y)

매개변수
x : 보간할 데이터 포인트들의 x좌표를 담은 배열, 리스트
y : 보간할 데이터 포인트들의 y좌표를 담은 배열, 리스트
tck : 생성된 B-스플라인을 나타내는 튜플(T,c,k) 반환, 나중에 spi.splev 함수 등을 사용해 보간된 값을 계산하는데 사용.
"""


"""
spi.splev 함수는 Scipy 서브패키지인 scipy.interpolate 에서 제공되는 함수로
B-스플라인을 평가하는데 사용

tck = spi.splrep(x, y)
x_new = [1.5, 2.5, 3.5]
y_interp = spi.splev(x_new, tck)

매개변수
x : 배열형태의 입력 값으로, B-스플라인을 평가할 위치를 나타내는 값이 들어있는 배열, 보간할 새로운 x값들의 배열을 전달
tck : B-스플라인을 나타내는 튜플로 보통 spi.splrep 함수로부터 반환됨.
der : 정수값으로 B-스플라인의 미분차수를 지정 (기본값 0)
"""

import scipy.interpolate as spi #Scipy 서브패키지 import

x = np.linspace(-2 * np.pi, 2*np.pi, 25)
def f(x):
    return np.sin(x) + 0.5 * x
ipo = spi.splrep(x, f(x), k=1) # 선형 스플라인 보간 구현
iy = spi.splev(x, ipo) # 보간된 값 유도

np.allclose(f(x), iy) # 보간된 값이 원래 함숫값과 비슷한지 확인 (True, False 반환)

create_plot([x,x] , [f(x), iy], ['b' , 'ro'] ,
           ['f(x)', 'interpolation'] , ['x' , 'f(x)'])

선형 스플라인 보간 (전체 데이)

 

 

 

선형 스플라인 보간과 3차 큐빅 스플라인 보간 비교

# 선형 스플라인 보간

xd = np.linspace(1.0 , 3.0, 50) #구간 수를 늘리고 간격을 더 작게 조정
iyd = spi.splev(xd, ipo)

create_plot([xd, xd] , [f(xd) , iyd] , ['b', 'ro'],
			['f(x)' , 'interpolation'] , ['x', 'f(x)'])

선형 스플라인 보간 (일부 데이터)

 

# 3차 큐빅 스플라인 보간

ipo = spi.splrep(x , f(x), k=3) # 전체 데이터에 대한 3차 스플라인 보간
iyd = spi.splev(xd, ipo) # 더 작은 구간에 대해 결과 적용

np.allclose(f(xd) , iyd) # 보간 결과가 완벽하진 않음 (False 리턴)

np.mean((f(xd) - iyd) ** 2) # 이전 선형보간보다 나아진 값 확인

create_plot([xd, xd] , [f(xd) , iyd] , ['b', 'ro'],
            ['f(x)' , 'interpolation'] , ['x', 'f(x)'])

 

3차 스플라인 보간 (일부 데이터)

 

 

 

=> 스플라인 보간법 적용 시 최소자승회귀법보다 더 정확한 근사 결과를 얻을 수 있으나, 데이터가 정렬 되어있고 잡음이 없어야 하며 다차원 문제는 적용할 수 없다. 또한 계산량이 더 많기 때문에 어떤 경우는 회귀법보다 훨씬 계산 시간이 오래 걸릴 수 있다.

 

 

최적화

 

금융, 경제 분야에서 최적화(convex optivization)는 중요한 역할 수행.

옵션 가격 계산을 위해 시장 데이터를 기반으로 인수 교정을 하거나,  대리인의 효용 함술ㄹ 최적화하는 경우 등에 진행한다.

 

1. 전역 최적화 

#최적화 함수 예시로 fm 함수 정의

def fm(p) :
    x,y = p
    return (np.sin(x) + 0.05 * x ** 2
            + np.sin(y) + 0.05 * y **2)


#주어진 x,y 구간에서의 함수 형태 (여러개의 국소 최소점 존재)
x = np.linspace(-10, 10, 50)
y = np.linspace(-10, 10, 50)
X, Y = np.meshgrid(x,y)
Z = fm((X,Y))

fig = plt.figure(figsize = (10,6))
ax = fig.gca(projection = '3d')
surf = ax.plot_surface(X, Y, Z, rstride=2, cstride=2,
                      cmap = 'coolwarm' , linewidth = 0.5,
                      antialiased = True)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x,y)')
fig.colorbar(surf, shrink=0.5, aspect=5)

 

선형 스플라인 보간 결과 시각화

 

 

- 전역 최적화 진행 :  Scipy 라이브러리의 하위 라이브러리인 scipy.optimize에 있는 brute 사용

"""
scipy.optimize.brute(func, ranges, args=(), Ns=20, full_output=False, finish=None)

매개변수
func : 최적화하려는 함수, 배열 형태의 인수를 받아서 스칼라 값을 반환
ranges : 각 매개변수에 대한 최적화 범위를 정의하는 튜플의 리스트, 각 튜플은 매개변수의 최소값과 최대값을 지정
args : func에 전달할 추가적인 인수, 기본값은 빈 튜플
Ns : 각 매개변수를 나눌 샘플 수, 기본값은 20
full_output : 최적화에 대한 자세한 정보를 반환할지 여부를 나타내는 boolean 값, 기본값은 False
finish : 정밀한 최적화를 위해 사용할 메소드로 기본값은 None

scipy.optimize.brute() 함수는 모든 가능한 조합을 계산하므로 간단한 경우에만 사용하는 것이 좋음.
"""


#무차별 대입 최적화 방법을 사용해 2차원 함수 'fo'를 최적화
import scipy.optimize as sco

def fo(p):
    x, y = p
    z = np.sin(x) + 0.05 * x ** 2 + np.sin(y) + 0.05 * y ** 2
    if output == True: 
        print('%8.4f | %8.4f | %8.4f' % (x, y, z))
    return z

output = True  # 출력 활성화
# -10에서 10.1까지 범위에서 스텝이 5인 값들 탐색, 추가적 최적화 사용안함
sco.brute(fo, ((-10, 10.1, 5), (-10, 10.1, 5)), finish=None)

step 5로 했을때 결과, 출력 x=y=0 최적 인숫값, 함숫값 0

 

output = True  # 출력 활성화
#step을 0.1로 조정
opt1 = sco.brute(fo, ((-10, 10.1, 0.1), (-10, 10.1, 0.1)), finish=None)

opt1 # 최적화 된 값을 포함한 튜플이 저장

fm(opt1) # 최적화된 값을 입력으로 받아 해당 값을 계산하는 함수

opt1 결과 출력
fm(opt1) 결과 출력
step 0.1로 했을때 결과, 출력 x=y=-1.4 최적 인숫값, 함숫값 -1.7749

 

 

 

2. 국소 최적화

 

"""
fmin()함수는 초기 추정값 x0에서 시작해 최적의 해를 찾아나감, 함수의 값을 최소화하는 x를 반환

scipy.optimize.fmin(func, x0, args=(), xtol=0.0001, ftol=0.0001, maxiter=None, maxfun=None, full_output=False, disp=True, retall=False, callback=None)

매개변수
func : 최적화할 함수, 최소화하려는 실수 값을 반환
x0 : 최적화를 시작할 초기 추정값
args : func에 전달할 추가적인 인수, 기본값은 빈 튜플
xtol : x의 허용 오차, 기본값은 0.0001
ftol : 함수값의 허용 오차, 기본값은 0.0001
maxiter : 최대 반복 횟수, 기본값은 None으로 반복횟수에 대한 제한 없음
maxfun : 최대 함수 호출 횟수, 기본값은 None
full_output : 최적화에 대한 자세하 정보를 반환할지 여부를 나타내는 boolean 값
disp : 최적화 과정에서 메시지 표시 여부 boolean 값
retall : 모든 반복에서 계산된 모든 추정값을 반환할지 여부를 나타내는 boolean 값
callback : 각 반복마다 호출되는 콜백함수, 기본값 None
"""

output = True
opt2 = sco.fmin(fo, opt1, xtol=0.001, ftol=0.001, maxiter=15, maxfn=20)

 

 

대개 최적화 문제에서 국소 최솟값을 구하기 전에 전역 최소화를 진행하는것을 권장.