من أصل واحد إلى أصول كثيرة
كل ما سبق نظر في سلسلة عوائد واحدة. أما المحفظة فتضم أصولًا كثيرة، ولا يعتمد خطرها على خطر كل أصل فقط بل على كيفية حركتها *معًا*. والتغاير بين أصلين هو متوسط حاصل ضرب انحرافيهما عن المتوسط: موجب إن مالا إلى الصعود والهبوط معًا، وسالب إن صعد أحدهما بينما هبط الآخر. وبالنسبة إلى n أصلًا توجد n * n تغايرات تُرتَّب في مصفوفة تغاير، وهي متماثلة وعلى قطرها التباينات.
import numpy as np
# Five days of returns for two assets: shape (days, assets)
returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])
centered = returns - returns.mean(axis=0) # subtract each column's mean
cov = centered.T @ centered / (len(returns) - 1) # (assets x days) @ (days x assets) -> (assets x assets)
print(np.round(cov, 6))
print(np.allclose(cov, np.cov(returns, rowvar=False))) # Trueمصفوفة التغاير للعينة
مع جدول عوائد بالشكل (days, assets) تصبح المصفوفة كلها سطرًا واحدًا من الجبر الخطي. نطرح متوسط كل عمود (axis=0، المتوسط عبر الأيام)، ونضرب منقول الجدول المركّز في نفسه: العنصر (i, j) في centered.T @ centered هو مجموع x_i * x_j عبر الأيام. ثم نقسم على days - 1 كما في تباين العينة. وهذا ما يحسبه np.cov(returns, rowvar=False) وDataFrame.cov()؛ لاحظ rowvar=False لأن np.cov يتوقع الأصول في الصفوف.
الارتباط: التغاير بلا وحدات
تغاير قدره 0.0004 لا يقول الكثير: فهو يعتمد على مدى تقلب الأصلين. والقسمة على حاصل ضرب الانحرافين المعياريين تزيل المقياس وتعطي الارتباط الذي يقع دائمًا بين -1 و1: corr[i, j] = cov[i, j] / (std[i] * std[j]) حيث std الجذر التربيعي للقطر. وفي الكود يبني np.outer(std, std) جدول الحواصل وتعيد قسمة واحدة تحجيم المصفوفة كلها بآحاد على القطر. يخبرنا الارتباط بمدى تلازم حركة أصلين؛ أما التغاير الذي يحتفظ بالمقياس فهو ما تحتاجه صيغ المخاطر.
# From covariance to correlation and annualised volatilities
import numpy as np
returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])
cov = np.cov(returns, rowvar=False)
std = np.sqrt(np.diag(cov))
corr = cov / np.outer(std, std)
print(np.round(corr, 4))
print(np.round(np.sqrt(np.diag(cov) * 252), 4)) # daily variances scaled to a year: volatility grows with sqrt(252)هل يمكن أن تكون هذه المصفوفة حقيقية؟
جدول أرقام عشوائي ليس مصفوفة تغاير. فيجب أن تكون متماثلة وشبه معرّفة موجبة: فمن أجل أي أوزان w فإن العدد w' C w هو تباين محفظة فلا يمكن أن يكون سالبًا. وبصورة مكافئة لا توجد قيمة ذاتية سالبة. المصفوفات المقدَّرة تجتاز الاختبار، أما المعدّلة يدويًا أو المركّبة من ارتباطات من مصادر مختلفة (مثل A~B = 0.9 وB~C = 0.9 وA~C = -0.9) فتفشل. ويُفحص ذلك بـ np.linalg.eigvalsh للمصفوفات المتماثلة وهي تعيد قيمًا ذاتية حقيقية، مع تسامح صغير للتقريب.
# A validity check: symmetry and no negative eigenvalue
import numpy as np
def is_valid_cov(matrix, tol=1e-10):
symmetric = bool(np.all(np.abs(matrix - matrix.T) <= tol))
return symmetric and bool(np.linalg.eigvalsh(matrix).min() >= -tol)
good = np.array([[1.0, 0.5, 0.2], [0.5, 1.0, 0.3], [0.2, 0.3, 1.0]])
bad = np.array([[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]])
print(is_valid_cov(good), np.round(np.linalg.eigvalsh(good), 3))
print(is_valid_cov(bad), np.round(np.linalg.eigvalsh(bad), 3))# Few days, many assets: the sample covariance matrix is singular
import numpy as np
rng = np.random.default_rng(0)
returns = rng.normal(0, 0.01, size=(5, 8)) # 5 days, 8 assets
cov = np.cov(returns, rowvar=False)
print(np.linalg.matrix_rank(cov), "of", cov.shape[0]) # 4 of 8: some portfolios seem to have zero variance- مصفوفة التغاير لجدول
(days, assets)هيcentered.T @ centered / (days - 1): متماثلة والتباينات على القطر. - الارتباط هو
cov / np.outer(std, std): بلا وحدات، بين -1 و1، وآحاد على القطر؛ وتُسنَّى التباينات بـperiodsوالتقلبات بجذره. - مصفوفة التغاير الصالحة متماثلة بلا قيم ذاتية سالبة (
np.linalg.eigvalsh)؛ وقد تخرق التقديرات من أيام قليلة أو المعدلة يدويًا ذلك.
التالي: عائد المحفظة وتباينها، تحويل الأوزان وهذه المصفوفة إلى خطر المحفظة.
المختبر: ما الذي يُقيَّم
- ابنِ جدول التغاير باستخدام
cov_matrix25 نقطة - حوّلها إلى ارتباطات باستخدام
corr_from_cov25 نقطة - اقرأ تقلب كل أصل باستخدام
annualised_vols25 نقطة - اختبر إن كانت ممكنة فعلًا باستخدام
is_valid_cov25 نقطة