الخسارة التي تشعر بها فعلًا
يعامل التقلب الصعود والهبوط بالتساوي، لكن المستثمر يعيش برقم آخر: كم يقع حسابه تحت أفضل قيمة بلغها. هذا هو التراجع (drawdown). فإذا ارتفعت محفظة إلى 110 وتقف الآن عند 98 فهي أدنى من قمتها بـ 10.9% أيًّا كان ما يقوله التقلب اليومي. وأقصى تراجع هو أعمق حفرة في التاريخ، والزمن تحت الماء هو المدة التي استغرقها الخروج منها. يغادر العملاء ويخفّض مديرو المخاطر الحدود عند التراجعات لا عند الانحرافات المعيارية.
import pandas as pd
prices = pd.Series(
[100, 105, 103, 98, 104, 110, 108, 107, 111, 95], dtype=float,
index=pd.bdate_range("2026-03-02", periods=10),
)
peak = prices.cummax() # the highest price so far, day by day
drawdown = prices / peak - 1 # 0 on a new high, negative below it
print(peak.tolist()) # [100.0, 105.0, 105.0, 105.0, 105.0, 110.0, 110.0, 110.0, 111.0, 111.0]
print(drawdown.round(4).tolist()) # [0.0, 0.0, -0.019, -0.0667, -0.0095, 0.0, -0.0182, -0.0273, 0.0, -0.1441]أعمق حفرة
تعطي cummax() القمة الجارية باستدعاء واحد. وقسمة السعر عليها وطرح 1 يعطيان التراجع في كل يوم، صفرًا أو سالبًا دائمًا. وأقصى تراجع هو ببساطة أدنى قيمة في تلك السلسلة، ويخبرك idxmin() متى حدث. والتراجعات غير متماثلة بشكل مهم: خسارة 20% تحتاج ربح 25% للتعويض 1 / (1 - 0.20) - 1، وخسارة 50% تحتاج 100%. ولهذا يحارب مديرو المخاطر لإبقاء التراجعات ضحلة.
print(round(drawdown.min(), 4), drawdown.idxmin().date()) # -0.1441 2026-03-13 111 down to 95
# The climb back is steeper than the fall
for loss in (0.10, 0.20, 0.50):
print(loss, round(1 / (1 - loss) - 1, 3))
# 0.1 0.111
# 0.2 0.25
# 0.5 1.0الزمن تحت الماء
العمق نصف القصة والمدة نصفها الآخر. قد تخسر استراتيجية 8% فقط وتمضي ثلاث سنوات تحت قمتها القديمة، وهذا يختبر صبر أي مستثمر. يكون اليوم تحت الماء ما دام تراجعه سالبًا، وتنهي القمة الجديدة الفترة. ولإيجاد أطول فترة بـ pandas علّم أيام تحت الماء بمقارنة، ثم رقّم كل سلسلة من القيم المتساوية بـ (flag != flag.shift()).cumsum()، ثم عُدّ داخل كل سلسلة. وحلقة بسيطة تعدّ وتعيد التصفير تؤدي الغرض نفسه وتُقرأ بسهولة.
underwater = drawdown < 0 # True while below the running peak
run_id = (underwater != underwater.shift()).cumsum() # a new id each time the flag changes
streak = underwater.astype(int).groupby(run_id).cumsum() # 1, 2, 3, ... inside each underwater run
print(streak.tolist()) # [0, 0, 1, 2, 3, 0, 1, 2, 0, 1]
print(int(streak.max())) # 3 the longest wait for a new high, in days# Return divided by the depth of the worst hole: the Calmar ratio
annual_return = 0.12
print(round(annual_return / abs(drawdown.min()), 2)) # 0.83peak = prices.cummax()وdrawdown = prices / peak - 1: صفر عند قمة جديدة وسالب تحتها.- أقصى تراجع هو
drawdown.min()؛ وخسارةxتحتاج ربحًا1 / (1 - x) - 1للتعويض. - الزمن تحت الماء هو أطول سلسلة أيام بتراجع سالب؛ وأسوأ تراجع في اختبار رجعي حد أدنى لا ضمان.
التالي: مولّدات الأعداد العشوائية والبذور، أساس كل محاكاة.
المختبر: ما الذي يُقيَّم
- تتبّع أعلى سعر بلغته حتى الآن باستخدام
running_peak25 نقطة - قِس البعد عن القمة باستخدام
drawdown_series25 نقطة - اعثر على أسوأ هبوط باستخدام
max_drawdown25 نقطة - عُدّ أطول انتظار لقمة جديدة باستخدام
longest_underwater25 نقطة