پائتھون کا استعمال کرتے ہوئے اندرونی تجارت کا تجزیہ کیسے کریں: ایک سی ای او پرچیزنگ کیس اسٹڈی

جب کمپنی کے اسٹاک کی قیمت گرنے کے بعد سی ای او اسٹاک خریدتا ہے، تو یہ اعتماد کے ووٹ کے طور پر خریداری کو پڑھنے کے لیے پرکشش ہوتا ہے۔ کاروبار چلانے والے لوگ اوسط سرمایہ کار سے زیادہ جانتے ہیں، اس لیے ٹریڈنگ ایک سگنل کی طرح محسوس ہوتی ہے جس کی پیروی کی جاتی ہے۔

لیکن ایک واضح مسئلہ ہے۔ 20% سے زیادہ گرنے والے اسٹاک اکثر واپس اچھالتے ہیں یہاں تک کہ جب اندرونی لوگ کچھ نہیں خریدتے ہیں۔ اگر آپ صرف اس بات کی پیمائش کرتے ہیں کہ CEO کے خریدنے کے بعد کیا ہوتا ہے، تو آپ ریکوری سگنلز لینا ختم کر سکتے ہیں، جو پہلے سے ہی ان اسٹاکس میں عام ہیں جو گر چکے ہیں، اندرونی سگنل کے طور پر۔

اس ٹیوٹوریل میں، ہم اسے درست طریقے سے جانچنے کے لیے ایک ازگر کا ورک فلو بنائیں گے۔ فارم 4 ٹرانزیکشنز درآمد کریں، CEO کی خریداریوں کو الگ کریں، استعمال کے قابل ایونٹس میں بار بار فائل کرنے والی قطاروں کو سمیٹیں، تاریخی قیمتیں منسلک کریں، ڈرا ڈاؤن اور ریٹرن کا حساب لگائیں، اور پھر اسی اسٹاک کے لیے خریداری کی اقساط کا اسی طرح کی غیر خریداری کی تاریخوں سے موازنہ کریں۔

حتمی واپسی کی میز واحد دلچسپ حصہ نہیں ہے۔ یہ وہ سب کچھ ہے جس کی آپ کو گندی ریگولیٹری فائلنگ کو ڈیٹا سیٹس میں تبدیل کرنے کی ضرورت ہے جو منصفانہ موازنہ کی حمایت کر سکے۔ راستے میں، ہم ڈپلیکیٹ تجارتی قطاروں، ایک ہی سی ای او کی طرف سے بار بار کی جانے والی خریداریوں، تجارتی تاریخ کی چھانٹی، قیمت کی نامکمل تاریخ، اور ون ٹو ون کنٹرول میچنگ کو ہینڈل کریں گے۔

آخر میں، ہم ایک مکمل ایونٹ ریسرچ ورک فلو اور "سی ای او کے خریدنے کے بعد اسٹاک کی قیمت بڑھ گئی” سے زیادہ مفید جواب کے ساتھ ختم کریں گے۔

انڈیکس

شرطیں

اس ٹیوٹوریل کی پیروی کرنے کے لیے آپ کو جدید مالیات یا مقداری پس منظر کی ضرورت نہیں ہے۔ ازگر کی بنیادی تفہیم pandas یہ کافی ہونا چاہئے۔

شروع کرنے سے پہلے، درج ذیل کو چیک کریں:

  • Python کو مقامی طور پر انسٹال کریں یا کسی نوٹ بک ماحول جیسے Jupyter Notebook یا Google Colab تک رسائی حاصل کریں۔

  • ڈیٹا فریموں، فنکشنز، لوپس، اور API کی درخواستوں کا بنیادی علم۔

  • EODHD API کلید اسکرینر تک رسائی کے لیے، فارم 4 فائل کرنے اور EOD اختتامی پوائنٹس کو ریکارڈ کرنے کے لیے

  • ان اسٹاکس کی تعداد کو پورا کرنے کے لیے کافی API کریڈٹس جنہیں آپ تجزیہ کرنے کے لیے منتخب کرتے ہیں۔

پورے کیس اسٹڈی میں 500 سیکیورٹیز سے فارم 4 ڈیٹا استعمال ہوتا ہے۔ اگر آپ بہت ساری API کالز کیے بغیر کوڈ کو سمجھنا چاہتے ہیں، تو آپ پہلے چھوٹے نمونے پر ورک فلو چلا سکتے ہیں۔

ایونٹ کے مطالعہ یا کنٹراسٹ مماثلت کے بارے میں پہلے سے علم کی ضرورت نہیں ہے۔ ہم ان حصوں کو مرحلہ وار بنائیں گے جیسا کہ تجزیہ میں ظاہر ہوتا ہے۔

مطلوبہ پیکیج حاصل کریں۔

ایک مکمل ورک فلو کے لیے پیکجوں کے صرف ایک چھوٹے سیٹ کی ضرورت ہوتی ہے۔ requests API کالوں کو ہینڈل کریں، pandas اور NumPy SciPy ڈیٹا کا زیادہ تر کام کرتا ہے اور ایک سے ایک مماثل الگورتھم فراہم کرتا ہے جو بعد میں کنٹرول گروپس کے لیے استعمال ہوتا ہے۔

import json
import re
import numpy as np
import pandas as pd
import requests
from scipy.optimize import linear_sum_assignment

یہ مکمل سیٹ اپ ہے۔ اضافی لائبریریوں یا غیر ضروری ٹولز کو شامل کیے بغیر صرف وہی حاصل کریں جو آپ کو اپنے تجزیے کے لیے درکار ہے۔ ان پیکجوں کو استعمال کرتے ہوئے انسٹال کریں: pip ماحول میں درآمد کرنے سے پہلے

اپنی اسٹاک کی دنیا بنائیں

اپنی اندرونی فائلنگ حاصل کرنے سے پہلے، آپ کو تلاش کرنے کے لیے کمپنیوں کی فہرست کی ضرورت ہوگی۔

ایک مارکیٹ کیپٹلائزیشن سیکٹر کے ساتھ شروع کرنے کے بجائے، ہم مائیکرو، مڈ اور لاج کیپ اسٹاکس میں ایک مرکب کائنات بنائیں گے۔ یہ تجزیہ میں تھوڑا سا تغیر فراہم کرتا ہے، بجائے اس کے کہ مارکیٹ کا ایک حصہ نمونے پر حاوی ہو۔

مارکیٹ کیپٹلائزیشن کی بالٹیاں ہیں:

  • micro_cap: $50 ملین ~ $300 ملین

  • small_cap: USD 300 ملین سے USD 2 بلین

  • mid_cap: USD 2 بلین سے USD 10 بلین

  • large_cap: 10 بلین ڈالر سے زیادہ

ہر بالٹی کے لیے، ہم 500 اسکرینر کے نتائج لیتے ہیں، تصادفی طور پر 250 کو منتخب کرتے ہیں، اور انہیں 1,000 اسٹاک کائنات میں جوڑ دیتے ہیں۔

def fetch_stocks(filters, cap):
    api_key = 'YOUR EODHD API KEY'
    base_url="https://eodhd.com/api/screener"
    all_stocks = []
    for i in range(0,500,100):
        params = {
            "api_token": api_key,
            "filters": json.dumps(filters),
            "sort": "market_capitalization.desc",
            "limit": 100,
            "offset": i}
        resp = requests.get(base_url, params = params).json()
        stocks = list(pd.DataFrame(resp['data'])['code'])
        all_stocks.append(stocks)
    all_stocks = [item for sublist in all_stocks for item in sublist]
    df = pd.DataFrame(columns = ['ticker', f'cap'])
    df.ticker, df.cap = all_stocks, cap
    df = df.sample(n = 250, random_state = 42)
    return df

micro_filters = [
    ["exchange", "=", "us"],
    ["market_capitalization", ">=", 50_000_000],
    ["market_capitalization", "<", 300_000_000]
]

small_filters = [
    ["exchange", "=", "NYSE"],
    ["market_capitalization", ">=", 300_000_000],
    ["market_capitalization", "<", 2_000_000_000]
]

mid_filters = [
    ["exchange", "=", "NYSE"],
    ["market_capitalization", ">=", 2_000_000_000],
    ["market_capitalization", "<", 10_000_000_000]
]

large_filters = [
    ["exchange", "=", "NYSE"],
    ["market_capitalization", ">=", 10_000_000_000]
]

micro_stocks = fetch_stocks(micro_filters, 'micro_cap')
small_stocks = fetch_stocks(small_filters, 'small_cap')
mid_stocks = fetch_stocks(mid_filters, 'mid_cap')
large_stocks = fetch_stocks(large_filters, 'large_cap')

frames = [micro_stocks, small_stocks, mid_stocks, large_stocks]
stocks_1000 = pd.concat(frames, ignore_index = True)
stocks_1000 = stocks_1000.sample(frac = 1, random_state = 42).reset_index(drop = True)
stocks_1000

میمو: تبدیلی YOUR EODHD API KEY اپنی اصلی EODHD API کلید استعمال کریں۔ اگر آپ کے پاس نہیں ہے تو، آپ اسے EODHD ڈویلپر اکاؤنٹ کھول کر حاصل کر سکتے ہیں۔

اسکرینر فی درخواست زیادہ سے زیادہ 100 قطاریں واپس کرتا ہے، لہذا لوپ پہلے 500 نتائج میں پانچ بیچوں میں منتقل ہوتا ہے۔

پھر ہم ان امیدواروں سے 250 ٹکرز کا نمونہ لیتے ہیں۔ ایک مقررہ بے ترتیب بیج انتخاب کو دوبارہ قابل بناتا ہے، لہذا سیل کو دوبارہ چلانے سے ایک جیسے نمونے نکلیں گے۔ اس کے بعد ہم چار مارکیٹ کیپٹلائزیشن فلٹرز کی وضاحت کرتے ہیں اور ان میں سے ہر ایک پر فنکشن چلاتے ہیں۔

حتمی ڈیٹا فریم میں 1,000 ٹکرز ہیں، ہر بالٹی میں 250۔

اب ایک انتباہ کا ذکر کرنا ضروری ہے۔ مائیکرو کیپ فلٹرز فلٹرز کی وسیع رینج کا استعمال کرتے ہیں۔ us ایک ایکسچینج قائم کریں جب کوئی دوسرا گروپ اسے استعمال کر رہا ہو۔ NYSE. اگرچہ یہ کیس اسٹڈی میں استعمال ہونے والا اسکرینر نمونہ ہے، لیکن اسے پوری امریکی اسٹاک مارکیٹ کا بالکل نمائندہ نمونہ نہیں سمجھا جانا چاہیے۔

سی ای او کی خریداریاں حاصل کریں اور تاریخ کا فلٹر لگائیں۔

اسٹاک کی دنیا کے تیار ہونے کے بعد، آپ EODHD کی انسائیڈر ٹریڈنگ (SEC Form 4) API کا استعمال کرتے ہوئے CEO کی خریداریوں کے لیے فارم 4 فائلنگ تلاش کرنا شروع کر سکتے ہیں۔

فارم 4 ڈیٹا میں داخلی خریداریوں سے زیادہ شامل ہے۔ فائلنگز میں سیلز، معاوضہ، اختیاری مشقیں، مشتق ٹرانزیکشنز، اور ایک ہی لین دین سے تعلق رکھنے والی متعدد قطاریں شامل ہو سکتی ہیں۔ لہذا آپ آسانی سے ہر دستاویز کو ڈاؤن لوڈ نہیں کرسکتے ہیں اور ہر ریکارڈ کو خرید سگنل کے طور پر نہیں دیکھ سکتے ہیں۔

اس تجزیہ کے لیے، ایک لین دین کو درج ذیل تمام شرائط کو پورا کرنا ضروری ہے:

  • غیر مشتق لین دین کے تحت ظاہر ہوتا ہے۔

  • انچارج شخص سے رپورٹ حاصل کریں۔

  • اس شخص کے پاس ایک ایگزیکٹو ٹائٹل ہونا ضروری ہے جو اس کی شناخت بطور CEO کرتا ہو۔

  • ٹرانزیکشن کوڈ استعمال کریں۔ P

  • حاصل شدہ اسٹاک کی نمائندگی کریں۔

  • اسٹاک اور قیمت دونوں میں مثبت اقدار پر مشتمل ہے۔

  • براہ کرم عام اسٹاک کا حوالہ دیں۔

ہم لین دین کی تاریخیں اور جمع کرانے کی تاریخیں بھی برقرار رکھتے ہیں۔ ٹریڈنگ کی تاریخ ہمیں بتاتی ہے کہ CEO نے اسٹاک کب خریدا، اور فائلنگ کی تاریخ ہمیں بتاتی ہے کہ بیرونی سرمایہ کار خریداری کب دیکھ سکتے ہیں۔ بعد کے تجزیے میں، جمع کرانے کی تاریخ سگنل کی تاریخ بن جاتی ہے۔

اگلا بلاک پورے نکالنے کو سنبھالتا ہے۔ فلٹرنگ فنکشن کی وضاحت کریں، اسے پہلے 500 اسٹاکس پر چلائیں، اور تمام اہل قطاروں کو ایک ڈیٹا فریم میں جوڑیں۔

def fetch_ceo_purchases(ticker):
    try:
        api_key = 'YOUR EODHD API KEY'

        all_form4 = []

        for i in range(0,1000,100):
            form4_url = f'https://eodhd.com/api/sec-filings/{ticker}/form4?api_token={api_key}&page[limit]=100&page[offset]={i}'
            resp = requests.get(form4_url).json()['data']
            all_form4.append(resp)

        all_form4 = [item for sublist in all_form4 for item in sublist]

        all_purchases = []

        ceo_pattern = re.compile(
            r'bceob|chief executive officer|co-chief executive officer|co-ceo|chief exec officer',
            re.IGNORECASE
        )

        for filing in all_form4:
            footnote_map = {
                footnote['footnote_id']: footnote['text']
                for footnote in filing.get('footnotes', [])
            }

            for transaction in filing.get('non_derivative', []):
                officer_title = transaction.get('officer_title') or ''
                security_title = transaction.get('security_title') or ''
                shares_amount = transaction.get('shares_amount')
                price_per_share = transaction.get('price_per_share')

                is_ceo = bool(ceo_pattern.search(officer_title))

                is_purchase = (
                    transaction.get('is_officer') is True
                    and is_ceo
                    and transaction.get('transaction_code') == 'P'
                    and transaction.get('acquired_or_disposed') == 'A'
                    and shares_amount is not None
                    and shares_amount > 0
                    and price_per_share is not None
                    and price_per_share > 0
                    and 'common stock' in security_title.lower()
                )

                if not is_purchase:
                    continue

                linked_footnotes=" ".join(
                    footnote_map.get(footnote_id, '')
                    for footnote_id in transaction.get('footnote_ids', [])
                )

                all_purchases.append({
                    'ticker': ticker,
                    'accession_number': filing['accession_number'],
                    'filed_at': filing['filed_at'],
                    'transaction_date': transaction['transaction_date'],
                    'reporting_owner_cik': transaction['reporting_owner_cik'],
                    'reporting_owner_name': transaction['reporting_owner_name'],
                    'officer_title': officer_title,
                    'security_title': security_title,
                    'shares_amount': shares_amount,
                    'price_per_share': price_per_share,
                    'total_value': transaction.get('total_value'),
                    'shares_owned_after': transaction.get('shares_owned_after'),
                    'footnotes': linked_footnotes
                })

        return all_purchases
    except:
        return None

all_ceo_purchases = []

for ticker in stocks_1000.ticker[:500]:
    ticker = ticker + '.US'
    ceo_purchases = fetch_ceo_purchases(ticker)
    if ceo_purchases:
        all_ceo_purchases.extend(ceo_purchases)
        print(f'{len(ceo_purchases)} ceo purchases found in {ticker}')
    else:
        print(f'no transaction found in {ticker}')
        
cp_df = pd.DataFrame(all_ceo_purchases)
cp_df.to_csv('ceo_purchases.csv')

یہ فنکشن 100 کے بیچوں میں فائل کرنے کی درخواست کرتا ہے اور واپس کیے گئے صفحات کو ایک فہرست میں ضم کرتا ہے۔ اس کے بعد ہم اپنے CEO خریداری کے قواعد کے مطابق تمام غیر اخذ کردہ لین دین کی جانچ کرتے ہیں۔

چونکہ ہم تمام دستاویزات میں بالکل یکساں عنوانات کا استعمال نہیں کرتے ہیں، اس لیے ہمارا CEO ٹائٹل چیک ریگولر ایکسپریشنز کا استعمال کرتا ہے۔ ایک سی ای او اس طرح ظاہر ہو سکتا ہے: CEO, Chief Executive Officerیا Co-CEOلہذا، اگر آپ بالکل ایک سٹرنگ سے مماثل ہیں، تو آپ ایک درست ریکارڈ سے محروم ہوجائیں گے۔

یہ منسلک فوٹ نوٹ کو بھی محفوظ رکھتا ہے۔ ٹرانزیکشن کوڈ P یہ مفید ہے، لیکن یہ مکمل کہانی خود نہیں بتاتا۔ فوٹ نوٹ یہ ظاہر کر سکتے ہیں کہ خریداری کا تعلق کسی لین دین کے منصوبے، تجویز، یا دوسرے معاہدے سے ہے جس کے لیے قریب سے جانچ پڑتال کی ضرورت ہے۔

چونکہ فارم 4 اینڈ پوائنٹ API کریڈٹس کو تیزی سے استعمال کر سکتا ہے، اس لیے ہم نے یہ مرحلہ کائنات میں پہلی 500 سیکیورٹیز کے لیے چلایا۔ بڑے نمونوں کے لیے، ایک ہی لوپ کو تمام 1,000 اسٹاک تک بڑھایا جا سکتا ہے۔

ایک بار قطاریں جمع ہوجانے کے بعد، ہم ڈیٹاسیٹ کو 2022 کے اوائل اور 2025 کے آخر کے درمیان فائل کردہ دستاویزات تک محدود کردیتے ہیں۔

cp_df = cp_df[cp_df["filed_at"].between("2022-01-01","2025-12-31")]

cp_df.tail()

سی ای او خریداری

یہ اب بھی ایک آزاد سی ای او کی طرف سے خریدنے کے سگنل کے بجائے ایک خام فائلنگ لائن ہے۔ اگر اسٹاک کے مختلف بلاکس مختلف قیمتوں پر حاصل کیے جاتے ہیں، تو ایک خریداری کو متعدد قطاروں میں تقسیم کیا جا سکتا ہے۔ اگلا مرحلہ اس ٹکڑے کو روزانہ خریداری کے ایونٹ میں سمیٹنا ہے۔

فارم 4 قطاروں کو روزانہ خریداری کے واقعات میں تبدیل کریں۔

ایک سی ای او ایک ہی دن متعدد قیمتوں پر اسٹاک خرید سکتا ہے۔ فائلنگ ہر قیمت بلاک کو الگ قطار کے طور پر ریکارڈ کرنے کی اجازت دیتی ہے، چاہے وہ قطار ایک وسیع خریداری سے تعلق رکھتی ہو۔ ہر قطار کو ایک آزاد سگنل کے طور پر علاج کرنے سے مصروف خریداری کے دنوں کو دوسرے خریداری کے دنوں کے مقابلے میں بہت زیادہ وزن حاصل ہوتا ہے کیونکہ وہ زیادہ قیمت کی سطحوں میں تقسیم ہوتے ہیں۔

لہذا اگلا مرحلہ ان قطاروں کو گروپ کرنا ہے جو ایک جیسے مواد کا اشتراک کرتی ہیں۔ ٹکر, سی ای او, فائلنگاور لین دین تاریخ

ہر گروپ کے لیے، حصہ اور خریداری کی کل قیمت شامل کریں۔ پھر ہم وزنی اوسط قیمت کا حساب لگاتے ہیں۔

وزنی اوسط قیمت = خریداری کی کل قیمت / خریدے گئے کل حصص

اگلا بلاک ایک مکمل جمع کرتا ہے اور فی یومیہ CEO خریداری کے ایونٹ میں ایک قطار تیار کرتا ہے۔

cp_df['purchase_value'] = (
    cp_df['shares_amount'] * cp_df['price_per_share']
)

group_columns = [
    'ticker',
    'reporting_owner_cik',
    'reporting_owner_name',
    'officer_title',
    'accession_number',
    'filed_at',
    'transaction_date'
]

daily_events = (
    cp_df.groupby(
        group_columns,
        as_index=False,
        dropna=False
    )
    .agg(
        shares_purchased=('shares_amount', 'sum'),
        total_purchase_value=('purchase_value', 'sum'),
        transaction_rows=('shares_amount', 'size'),
        shares_owned_after=('shares_owned_after', 'max')
    )
)

daily_events['weighted_average_price'] = (
    daily_events['total_purchase_value']
    / daily_events['shares_purchased']
)

daily_events.filed_at = pd.to_datetime(daily_events.filed_at)
daily_events.to_csv('ceo_purchases_grouped.csv')
daily_events.tail()

کہ purchase_value کالم جمع ہونے سے پہلے تمام خام قطاروں کو ڈالر کی قیمت فراہم کرتا ہے۔ ایک بار قطاروں کے گروپ ہونے کے بعد، آپ مختلف قیمتوں کی خریداری کے اثرات کو کھونے کے بغیر ان کی قدروں کو جمع کر سکتے ہیں۔

کہ transaction_rows گرمی اس بات کا تعین کرنے میں مفید ہے کہ کتنی خرابی ہوئی ہے۔ قدر 1 اس کا مطلب ہے کہ روزانہ کے واقعات پہلے ہی ایک قطار میں دکھائے جاتے ہیں۔ قدر 5 اس کا مطلب ہے کہ 5 علیحدہ جمع کرانے والی قطاروں کو ایک خریداری کے ایونٹ میں ملا دیا گیا ہے۔

گروپ شدہ سی ای او پرچیز ڈیٹاسیٹ

جمع نے ڈیٹا سیٹ کو 625 خام لین دین کی قطاروں سے 535 روزانہ خریداری کے واقعات تک کم کر دیا۔

فرق صرف اشعار میں نہیں ہے۔ تجزیہ کی اکائی کو "اطلاع شدہ قیمت کے بلاک کے 1 دن” سے "CEO کی خریداری کی تاریخ کے 1 دن” میں تبدیل کریں۔ یہ ان معاشی واقعات کے بہت قریب ہے جن کا ہم مطالعہ کرنے کی کوشش کر رہے ہیں۔

لیکن آپ ابھی تک اپنے منافع کا حساب لگانے کے لیے تیار نہیں ہیں۔ خریداری کا ایک واقعہ ہمیں بتاتا ہے کہ CEO نے خریداری کی ہے، لیکن یہ ہمیں یہ نہیں بتاتا ہے کہ آیا اسٹاک کی قیمت اپنے عروج کے قریب ہے، قدرے نیچے ہے، یا پہلے سے ہی گراوٹ میں گہری ہے۔ اگلا، میں ہر دستاویز کے جاری ہونے پر دستیاب قیمتوں کا سیاق و سباق منسلک کروں گا۔

تاریخی قیمتیں اور کمی شامل کریں۔

CEO کی خریداری صرف اس ٹیسٹ میں دلچسپ ہوتی ہے جب ہمیں معلوم ہوتا ہے کہ اس وقت اسٹاک کہاں ٹریڈ کر رہا تھا۔

اس کی سالانہ بلندی سے 5% نیچے اسٹاک خریدنا اسٹاک کے 40% گرنے کے بعد اسے خریدنے سے بہت مختلف ہے۔ لہذا اب ہمیں اپنے ورک فلو میں تاریخی قیمت کا ڈیٹا لانے کی ضرورت ہے تاکہ یہ اندازہ لگایا جا سکے کہ فائلنگ کو پبلک کرنے کے وقت ہر اسٹاک اپنی حالیہ بلند ترین سطح سے کتنا نیچے تھا۔

اصل اختتامی قیمت کے بجائے ایڈجسٹ شدہ اختتامی قیمت کا استعمال کریں کیونکہ ایڈجسٹ شدہ قیمت سٹاک سپلٹس اور ڈیویڈنڈز جیسے واقعات کے لیے ہوتی ہے۔ یہ وقت کے ساتھ قیمتوں کا موازنہ کرنے کے لیے ایک زیادہ مستقل سلسلہ فراہم کرتا ہے۔

اگرچہ خریداری کا تجزیہ 2022 میں شروع ہوتا ہے، ہم قیمتیں 2021 سے 2025 تک لیتے ہیں۔ 2022 میں پہلے مشاہدات کے لیے ایک اضافی سال درکار ہوگا کیونکہ ہمیں اب بھی ایک سال بعد چوٹی کا حساب لگانے کے لیے کافی پچھلے ڈیٹا کی ضرورت ہے۔

اگلا بلاک سی ای او پرچیز ڈیٹاسیٹ میں نمائندگی کرنے والے تمام ٹکرز کے لیے یومیہ تاریخی قیمتوں کے EODHD کے تاریخی EOD اختتامی پوائنٹس لیتا ہے اور انہیں ایک ڈیٹا فریم میں یکجا کرتا ہے۔

tickers = list(cp_df.ticker.unique())
historical_eod_entries = []

def fetch_historical_eod(ticker):
    api_key = 'YOUR EODHD API KEY'
    historical_url = f'https://eodhd.com/api/eod/{ticker}?from=2021-01-01&to=2025-12-31&period=d&api_token={api_key}&fmt=json'
    historical_resp = requests.get(historical_url).json()
    historical_filtered = []
    for item in historical_resp:
        item['ticker'] = ticker
        keys = ['ticker', 'date', 'adjusted_close']
        item = {key: item.get(key) for key in keys}
        historical_filtered.append(item)
    return historical_filtered

for ticker in tickers:
    try:
        historical_eod = fetch_historical_eod(ticker)
        historical_eod_entries.extend(historical_eod)
        print(f'{ticker} done')
    except:
        print(f'{ticker} error')

یہ کوڈ تاریخی ڈیٹا لیتا ہے اور فی ٹکر فی تجارتی دن ایک قیمت کی قطار فراہم کرتا ہے۔ اگلا، ہم رولنگ ہائیز اور ڈپس کا حساب لگاتے ہیں۔

پیچھے رہنے والی اونچائیوں اور زوال کا حساب لگائیں۔

ہر تجارتی دن کے لیے، ہم یہ جاننا چاہتے ہیں کہ پچھلے 252 تجارتی سیشنز کے دوران پہنچنے والی سب سے زیادہ ایڈجسٹ شدہ بند قیمت۔ یہ تقریباً ایک تجارتی سال ہے۔

اس کے بعد ڈرا ڈاؤن کا حساب اس طرح کیا جاتا ہے:

کمی = ایڈجسٹ شدہ اختتامی قیمت / پیچھے رہ جانا 252 دن کی اونچائی – 1

قدر -0.20 اس کا مطلب یہ ہے کہ اسٹاک کی قیمت اس کی بند ہونے والی بلندی سے 20% نیچے ہے۔ قدر -0.35 اس کا مطلب ہے کہ یہ اس چوٹی سے 35% نیچے ہے۔

اگلا بلاک ہر اسٹاک کی قیمت کی تاریخ کو ترتیب دیتا ہے، رولنگ ہائیز کا حساب لگاتا ہے، اور نتائج کو اعشاریہ اور فیصد کمی والے کالموں میں تبدیل کرتا ہے۔

historical_df = pd.DataFrame(historical_eod_entries)
historical_df['date'] = pd.to_datetime(historical_df.date)

historical_df['rolling_high_252'] = (historical_df.groupby('ticker')['adjusted_close'].transform
                                     (lambda prices: prices.rolling(window=252, min_periods=200).max()))

historical_df['drawdown'] = (historical_df['adjusted_close']/ historical_df['rolling_high_252']- 1)
historical_df['drawdown_pct'] = (historical_df['drawdown'] * 100)

کہ min_periods=200 دلیل فوری وضاحت کا متقاضی ہے۔

مکمل رولنگ پیریڈ میں 252 تجارتی دن شامل ہیں، لیکن بالکل 252 مشاہدات کی ضرورت بہت سی ابتدائی قطاروں کو ہٹا دیتی ہے۔ 200 سیشنز کے بعد حساب کی اجازت دینے کے لیے اب بھی تاریخی ڈیٹا کی ایک خاصی مقدار درکار ہوتی ہے، لیکن کچھ لچک فراہم کرتی ہے۔

کافی ریکارڈ کے بغیر قطاریں غائب رہتی ہیں بجائے اس کے کہ ایک کمزور انتشار کا تخمینہ وصول کیا جائے۔

ہر خریداری کو تازہ ترین دستیاب قیمت سے ملا دیں۔

اب ہمیں ہر سی ای او کی خریداری کے ایونٹ سے قیمت کا سیاق و سباق منسلک کرنے کی ضرورت ہے۔

جمع کرانے کی تاریخ سگنل کی تاریخ ہے، لیکن اسی تاریخ پر اختتامی قیمت استعمال نہیں کی جانی چاہیے۔ فارم 4 ٹریڈنگ سیشن سے پہلے، اس کے دوران، یا بعد میں داخل کیا جا سکتا ہے، اس لیے ہو سکتا ہے کہ فائل کے ظاہر ہونے کے وقت اس دن کی آخری تاریخ معلوم نہ ہو۔

اس کے بجائے، ہم جمع کرانے کی تاریخ سے پہلے حالیہ مکمل شدہ لین دین کی تاریخ استعمال کرتے ہیں۔

مندرجہ ذیل بلاک مندرجہ ذیل سے مماثل کچھ کرتا ہے: merge_asof(). باقاعدہ انضمام کے برعکس، ہر فائلنگ کو قریب ترین منتقلی کی قیمت کی تاریخ سے ملایا جا سکتا ہے بجائے اس کے کہ دو تاریخیں ایک جیسی ہوں۔

cp_df.filed_at = pd.to_datetime(cp_df.filed_at)

price_columns = ['ticker', 'date', 'adjusted_close', 'rolling_high_252', 'drawdown', 'drawdown_pct']

analysis_df = pd.merge_asof(
    cp_df.sort_values(['filed_at', 'ticker']),
    historical_df[price_columns].sort_values(['date', 'ticker']),
    by='ticker',
    left_on='filed_at',
    right_on='date',
    direction='backward',
    allow_exact_matches=False
)

analysis_df = analysis_df.rename(columns={'date': 'price_date'})

بنیادی ترتیبات یہ ہیں۔ allow_exact_matches=False. یہ آپ کو آپ کی 10 مارچ کی واپسی پر 10 مارچ کی اختتامی قیمت استعمال کرنے سے روکتا ہے۔ اس کے بجائے، انضمام 10 مارچ سے پہلے تازہ ترین دستیاب تجارتی دن کا استعمال کرتا ہے۔

ضم شدہ ڈیٹا فریم اب اس طرح لگتا ہے:

ضم شدہ ڈیٹاسیٹ

ضم شدہ ڈیٹا فریم میں اب خریداری کی معلومات کے ساتھ شامل ہیں:

  • price_date: میچ میں استعمال ہونے والی ٹریڈنگ کی تاریخ

  • adjusted_close: دن کی اسٹاک کی قیمت

  • rolling_high_252: پچھلے سال میں سب سے زیادہ قیمت

  • drawdown: اعشاریہ نمبروں میں کمی دکھائیں۔

  • drawdown_pct: فیصد کے طور پر وہی کمی دکھائیں۔

مثال کے طور پر، drawdown_pct قدر -32.4 اس کا مطلب ہے کہ رپورٹ سے پہلے آخری تجارتی دن اسٹاک اپنی 252 دن کی بلند ترین سطح سے 32.4% نیچے ہے۔

اب ہم دونوں جانتے ہیں کہ CEO نے خریدا اور جب خریداری عوامی ہو گئی تو اسٹاک کی قیمت کتنی گر گئی۔

اگلا مسئلہ دوبارہ خریداری کا ہے۔ ایک سی ای او جو کئی ہفتوں کے دوران متعدد خریداری کرتا ہے اسے خود بخود متعدد آزاد سگنل نہیں بنانا چاہیے۔

خریداری کے واقعات کو اقساط میں تبدیل کریں۔

اس وقت، ڈیٹا سیٹ میں ایک قطار فی CEO خریداری کی تاریخ ہے۔ یہ خام فارم 4 لائنوں کے ساتھ کام کرنے سے بہتر ہے، لیکن پھر بھی انہی بنیادی فیصلوں کا زیادہ حساب لگا سکتا ہے۔

تصور کریں کہ سی ای او پیر کو اسٹاک خریدتا ہے، اگلے ہفتے دوبارہ، اور دو ہفتے بعد دوبارہ۔ سختی سے بولیں، یہ تین خریداری کے واقعات ہیں۔ اقتصادی طور پر، یہ ایک مسلسل خریداری مہم ہو سکتی ہے۔

تینوں کو آزاد سگنل کے طور پر برتاؤ کرنے سے اکثر خریداروں کو ایک CEO کے مقابلے میں زیادہ وزن ملے گا جس نے ایک ہی لین دین میں ایک ہی خیال کو مکمل کیا۔ لہذا واپسیوں کا حساب لگانے سے پہلے، ہم قریبی خریداریوں کو اس میں گروپ کرتے ہیں: اقساط خریدیں۔.

اصول سادہ ہیں۔ ایک ہی اسٹاک کے ایک ہی سی ای او کی طرف سے خریداری ایک ایپی سوڈ بنتی ہے اگر فائلنگ کی مسلسل تاریخیں 28 کیلنڈر دنوں سے زیادہ نہیں ہوتی ہیں۔

سب سے پہلے، ہم دستیاب کمیوں کے بغیر خریداری کے واقعات کو ہٹاتے ہیں، باقی قطاروں کو ٹکر، سی ای او، اور جمع کرانے کی تاریخ کے حساب سے ترتیب دیتے ہیں، اور پچھلی خریداری کے بعد کے دنوں کی تعداد کا حساب لگاتے ہیں۔

purchase_events = analysis_df.dropna(subset=["drawdown"])
purchase_events.filed_at = pd.to_datetime(purchase_events.filed_at)
purchase_events = purchase_events.sort_values(["ticker", "reporting_owner_cik", "filed_at"])
purchase_events["days_since_previous"] = purchase_events.groupby(["ticker", "reporting_owner_cik"])["filed_at"].diff().dt.days
purchase_events["new_episode"] = purchase_events["days_since_previous"].isna() | (purchase_events["days_since_previous"] > 28)
purchase_events["episode_id"] = purchase_events.groupby(["ticker", "reporting_owner_cik"])["new_episode"].cumsum()

کسی بھی ٹکر سی ای او کے امتزاج کے لیے پہلی خریداری خود بخود ایک نیا ایپی سوڈ شروع کر دیتی ہے کیونکہ موازنہ کرنے کے لیے کوئی پچھلی گذارشات نہیں ہیں۔

اس کے بعد، ایک نیا ایپی سوڈ صرف اس صورت میں شروع ہوگا جب پچھلی درخواست کا وقفہ 28 دن سے زیادہ ہو۔ 28 دن یا اس سے کم کی خریداریاں اسی ایپی سوڈ میں رہتی ہیں۔

کل چل رہا ہے new_episode ہم ہر خریداری کے آرڈر کے لیے ایک منفرد شناخت کنندہ فراہم کرتے ہیں۔

اب چونکہ ہر خریداری کا واقعہ ایک ایپی سوڈ سے تعلق رکھتا ہے، ہم خریداری کے آرڈر کے ذریعے واقعات کو ایک قطار میں سمیٹ سکتے ہیں۔

ہر ایپی سوڈ کے لیے، ہم فائل کرنے کی پہلی اور آخری تاریخیں رکھتے ہیں، ایکویٹی اور خریداری کی قیمتوں میں اضافہ کرتے ہیں، خریداری کی سرگرمی کا حساب لگاتے ہیں، اور فائلنگ کی پہلی تاریخ سے ہونے والے نقصانات کو محفوظ رکھتے ہیں۔

episodes = purchase_events.groupby(["ticker", "reporting_owner_cik", "reporting_owner_name", "episode_id"], 
                               as_index=False).agg(first_filing_date=("filed_at", "min"), 
                                                   last_filing_date=("filed_at", "max"),
                                                   first_transaction_date=("transaction_date", "min"),
                                                   total_shares=("shares_purchased", "sum"),
                                                   total_purchase_value=("total_purchase_value", "sum"),
                                                   purchase_days=("filed_at", "nunique"),
                                                   transaction_events=("filed_at", "size"),
                                                   initial_drawdown=("drawdown", "first"), 
                                                   initial_drawdown_pct=("drawdown_pct", "first"))

یہاں دو سرگرمیاں شمار ہوتی ہیں:

کل حصص اور خریداری کی قیمت پوری قسط پر محیط ہے۔ تاہم، کمی پہلی فائلنگ کی تاریخ سے ہوتی ہے جب سگنل شروع ہوتا ہے۔

یہ تفصیل 20% فلٹر کے لیے اہم ہے۔

ہم کہتے ہیں کہ واقعہ اس وقت شروع ہوتا ہے جب اسٹاک کی قیمت اپنی چوٹی سے 18% نیچے ہوتی ہے، اور پھر کمی 24% تک پہنچنے کے بعد CEO دوبارہ خریدتا ہے۔ یہ کہنا گمراہ کن ہوگا کہ قسط 20 فیصد کمی کے بعد شروع ہوئی۔

لہذا، ہم پہلے ان کو گروپ کرتے ہیں اور پھر اس کا استعمال کرتے ہوئے ایک حد کا اطلاق کرتے ہیں: initial_drawdown.

episodes_20 = episodes[episodes["initial_drawdown"] <= -0.20].copy()

print("All purchase episodes:", len(episodes))
print("20% drawdown episodes:", len(episodes_20))
print("Tickers represented:", episodes_20["ticker"].nunique())

episodes_20

اقساط خریدیں۔

اقساط خریدیں۔

اب ہم نے دہرائی جانے والی خریداریوں کو 137 انفرادی CEO خریدنے والے ایپی سوڈز تک کم کر دیا ہے جو اس وقت شروع ہوا جب اسٹاک اپنی پچھلی بلندی سے کم از کم 20% نیچے تھا۔

یہ واقعہ ایک حقیقی اشارہ ہے جسے ہم وقت کے ساتھ ساتھ فالو کریں گے۔ اگلا، ہم ابتدائی رپورٹ کے بعد پہلے تجارتی دن میں داخل ہوں گے اور پیمائش کریں گے کہ 1 ماہ، 3 ماہ، 6 ماہ، اور 12 ماہ میں کیا ہوا ہے۔

سی ای او کی خریداری کے بعد منافع کا حساب لگائیں۔

اب 137 CEO خریدنے والے ایپی سوڈز ہو چکے ہیں جہاں اسٹاک اپنی 252 دن کی بلند ترین سطح سے کم از کم 20% نیچے شروع ہوا۔

اگلا سوال آسان ہے۔ ان دستاویزات کے جاری ہونے کے بعد کیا ہوا؟

ہم ایپی سوڈ کے پہلے داخلے کی تاریخ کے طور پر جمع کرائے جانے کے بعد پہلا تجارتی دن استعمال کرتے ہیں۔ یہ ٹیسٹ کو حقیقت پسندانہ رکھتا ہے۔ بیرونی سرمایہ کار اس وقت تک کارروائی نہیں کر سکتے جب تک کہ فارم 4 ظاہر نہ ہو جائے، اور ہم ہفتے کے آخر یا بازار کی چھٹیوں پر جمع کرائی گئی دستاویزات پر کارروائی کرنے کے لیے اگلے تجارتی سیشن کا بھی استعمال کرتے ہیں۔

آمدنی کی حدود درج ذیل ہیں:

  • 1 مہینہ: 21 تجارتی دن

  • 3 ماہ: 63 تجارتی دن

  • 6 ماہ: 126 تجارتی دن

  • 12 مہینے: 252 تجارتی دن

قیمت کی تفصیلات کو ٹکرز میں ترتیب دیں۔

ریٹرن کا حساب لگانے سے پہلے، ہم ہر اسٹاک کے لیے ایک الگ، تاریخی طور پر ترتیب دی گئی قیمت کی سیریز تیار کرتے ہیں۔

یہ واپسی کے فنکشن کو درست اندراج کی تاریخ تلاش کرنے اور پھر پورے تاریخی ڈیٹا فریم کو بار بار فلٹر کیے بغیر ٹریڈنگ سیشنز کی ایک مقررہ تعداد کو آگے بڑھانے کی اجازت دیتا ہے۔

episodes_20 = episodes_20.reset_index(drop = True)
episodes_20['first_filing_date'] = pd.to_datetime(episodes_20['first_filing_date'])
historical_df['date'] = pd.to_datetime(historical_df['date'])

prices = historical_df[['ticker', 'date', 'adjusted_close']].dropna().sort_values(['ticker', 'date'])
price_map = {ticker: group.reset_index(drop=True) for ticker, group in prices.groupby('ticker')}

price_map ایک لغت جہاں ہر ٹکر اپنی تاریخ کے ڈیٹا فریم اور ایڈجسٹ شدہ اختتامی قیمت کی طرف اشارہ کرتا ہے۔

مثال کے طور پر، price_map['AAT.US'] کے لیے صرف تاریخی قیمتیں شامل ہیں۔ AAT.USوہ پہلے سے ہی سب سے پرانے سے نئے میں ترتیب دیئے گئے ہیں۔

داخلے کی تاریخ تلاش کریں اور مستقبل کے منافع کا حساب لگائیں۔

اب آپ ایک ایسا فنکشن لکھ سکتے ہیں جو ایک وقت میں ایک پرچیزنگ ایپیسوڈ پر کارروائی کرتا ہے۔

یہ فنکشن مندرجہ ذیل کام کرتا ہے:

  1. اسٹاک کی قیمت کی تاریخ تلاش کریں۔

  2. جمع کرانے کی تاریخ کے بعد پہلا تجارتی دن تلاش کرتا ہے۔

  3. دن کی ایڈجسٹ شدہ اختتامی قیمت کو داخلہ قیمت کے طور پر محفوظ کریں۔

  4. 21، 63، 126، 252 تجارتی سیشنز کے ذریعے آگے بڑھیں۔

  5. ہر دور میں منافع کا حساب لگائیں۔

def calculate_forward_returns(row):
    ticker_prices = price_map.get(row['ticker'])

    if ticker_prices is None:
        return pd.Series(dtype="object")

    dates = ticker_prices['date'].to_numpy(dtype="datetime64[ns]")
    entry_index = np.searchsorted(dates, np.datetime64(row['first_filing_date']), side="right")

    if entry_index >= len(ticker_prices):
        return pd.Series(dtype="object")

    entry_date = ticker_prices.loc[entry_index, 'date']
    entry_price = ticker_prices.loc[entry_index, 'adjusted_close']

    result = {'entry_date': entry_date, 'entry_price': entry_price}
    horizons = {'1m': 21, '3m': 63, '6m': 126, '12m': 252}

    for label, days in horizons.items():
        target_index = entry_index + days

        if target_index < len(ticker_prices):
            target_price = ticker_prices.loc[target_index, 'adjusted_close']
            result[f'date_{label}'] = ticker_prices.loc[target_index, 'date']
            result[f'return_{label}'] = target_price / entry_price - 1
        else:
            result[f'date_{label}'] = pd.NaT
            result[f'return_{label}'] = np.nan

    return pd.Series(result)

forward_returns = episodes_20.apply(calculate_forward_returns, axis=1)
episode_returns = pd.concat([episodes_20.reset_index(drop=True), forward_returns], axis=1)

episode_returns

نتیجے میں آنے والے ڈیٹا فریم میں ایپی سوڈ کی تفصیلات، داخلے کی تاریخ، اندراج کی قیمت، اور ہر مدت کے لیے آگے کی واپسی شامل ہوتی ہے۔

سی ای او پرچیز لیڈنگ ریٹرن

خام آمدنی کا خلاصہ

اگرچہ انفرادی اقساط کو دیکھنا مفید ہے لیکن مختصراً پورے نمونے کو دیکھنا بھی ضروری ہے۔

ہر افق کے لیے ہم حساب لگاتے ہیں:

summary = []

for horizon in ['1m', '3m', '6m', '12m']:
    returns = episode_returns[f'return_{horizon}'].dropna()

    summary.append({
        'horizon': horizon,
        'observations': len(returns),
        'mean_return': returns.mean(),
        'median_return': returns.median(),
        'positive_rate': (returns > 0).mean()
    })

summary_df = pd.DataFrame(summary)
summary_df

آگے کی واپسی کا خلاصہ

پہلی نظر میں، نتائج امید افزا نظر آتے ہیں. اوسط واپسی 3 ماہ کے بعد 11.9% اور 12 ماہ کے بعد 35.4% تک پہنچ گئی۔ زیادہ تر 12 ماہ کے مشاہدات بھی مثبت تھے۔

لیکن یہ وہ جگہ ہے جہاں غلط نتیجہ اخذ کرنا آسان ہے۔

یہ نمبر ہمیں بتاتے ہیں کہ CEO کے خریدنے کے بعد کیا ہوا۔ وہ ہمیں یہ نہیں بتاتے کہ اس کارکردگی کا کتنا حصہ خود سی ای او کی خریداریوں سے آیا۔

اسٹاک کی قیمت پہلے ہی 20 فیصد سے زیادہ گر چکی ہے۔ ہو سکتا ہے ان میں سے کچھ نے صرف اس لیے واپس باؤنس کیا ہو کیونکہ پیٹا ہوا اسٹاک کبھی کبھی ٹھیک ہو جاتا ہے۔

ان دو اثرات کو الگ کرنے کے لیے ایک موازنہ گروپ کی ضرورت ہوتی ہے جس میں کمی کی اسی طرح کی تاریخیں شامل ہوں جس میں CEO کی کوئی خریداری قریب میں نہیں ہوئی۔

ایک غیر خریداری کنٹرول گروپ قائم کرنا

خام واپسی کی میز حوصلہ افزا لگ رہی تھی، لیکن پھر بھی سی ای او نے سارا کریڈٹ خرید لیا۔

یہ منصفانہ امتحان نہیں ہے۔ ہمارے نمونے میں موجود تمام اسٹاک پہلے ہی کم از کم 20% نیچے ہیں، اور جو گرتے ہیں وہ بغیر کسی اندرونی سرگرمی کے واپس اچھال سکتے ہیں۔ ہر CEO کی خریداری کے ایپی سوڈ کا موازنہ دوسری تاریخوں سے کیا جانا چاہیے جب ایک ہی اسٹاک اسی طرح کے دباؤ میں تھا لیکن قریب میں کوئی CEO خریداری نہیں ہوئی۔

ایک درست کنٹرول کو درج ذیل چھ اصولوں پر پورا اترنا چاہیے:

  • ایک ہی ٹکر

  • اسی سال

  • 5% پوائنٹس کے اندر گریں۔

  • 180 دنوں کے اندر

  • CEO کو 28 دنوں سے پہلے یا بعد میں نہیں خریدا جا سکتا

  • صرف ایک بار استعمال کریں۔

ہر CEO خریداری کا واقعہ بھی صرف ایک بار ملایا جاتا ہے۔

کنٹرول امیدوار بنائیں

آئیے 2022 اور 2025 کے درمیان تمام تجارتی دنوں کو تلاش کرکے شروع کریں جب اسٹاک کی قیمت اس کی پچھلی بلندی سے 20% سے زیادہ نیچے تھی۔

لیکن ایک مسئلہ ہے۔ اسٹاک مہینوں تک اس حد سے نیچے رہ سکتے ہیں۔ اگر ہم روزانہ تجارتی دن کو برقرار رکھتے ہیں، تو ایک طویل کمی سینکڑوں تقریباً ایک جیسے کنٹرول کے امیدوار پیدا کر سکتی ہے۔

اس کو روکنے کے لیے، ہم ہر مسلسل کمی کی مدت کو 28 دن کے بلاکس میں تقسیم کرتے ہیں اور ہر بلاک میں ایک امیدوار رکھتے ہیں۔

hist = historical_df[['ticker', 'date', 'adjusted_close', 'rolling_high_252', 'drawdown', 'drawdown_pct']].dropna(subset=['drawdown'])

hist['date'] = pd.to_datetime(hist['date'])
hist = hist[hist['date'].between('2022-01-01', '2025-12-31')].sort_values(['ticker', 'date'])

hist['below_20'] = hist['drawdown'] <= -0.20
hist['previous_below_20'] = hist.groupby('ticker')['below_20'].shift().fillna(False)
hist['new_state'] = hist['below_20'].ne(hist['previous_below_20'])
hist['drawdown_segment'] = hist.groupby('ticker')['new_state'].cumsum()

control_candidates = hist[hist['below_20']].copy()

control_candidates['segment_start'] = control_candidates.groupby(['ticker', 'drawdown_segment'])['date'].transform('min')
control_candidates['anchor_block'] = ((control_candidates['date'] - control_candidates['segment_start']).dt.days // 28)
control_candidates = control_candidates.sort_values(['ticker', 'date']).drop_duplicates(['ticker', 'drawdown_segment', 'anchor_block'])

control_candidates = control_candidates.reset_index(drop = True)
control_candidates

کنٹرول امیدوار

below_20 وہ تاریخ دکھاتا ہے جب کمی کی حد گزر گئی تھی۔

drawdown_segment پھر ہم ایک مسلسل کمی کو دوسرے سے الگ کرتے ہیں۔ اگر اسٹاک حد سے اوپر بحال ہوتا ہے اور بعد میں واپس دہلیز سے نیچے آجاتا ہے، تو وہ سیگمنٹ ایک نیا سیگمنٹ بن جاتا ہے۔

ہر طبقہ کے اندر anchor_block ہم زوال کے شروع ہونے کے دن سے 28 دن کی مدت کا حساب لگاتے ہیں۔ ایک قطار فی بلاک رکھنے سے آپ کو تاریخوں کا ایک قابل انتظام سیٹ حاصل کرنے کی اجازت ملتی ہے بغیر ہر سیشن کو نئے ایونٹ کی طرح کمی کے ساتھ سلوک کیے بغیر۔

یہ تاریخیں صرف ممکنہ کنٹرولز ہیں۔ ہمیں اب بھی ہر چیز سے چھٹکارا حاصل کرنے کی ضرورت ہے جو سی ای او کی خریداری کے ساتھ ہوا تھا۔

سی ای او کی خریداریوں کے قریب تاریخیں ہٹا دیں۔

بغیر خریداری کے کنٹرول اندرونی سگنلز سے مکمل طور پر الگ ہونے چاہئیں۔

مثال کے طور پر، CEO فائل کرنے سے تین دن پہلے واپسی کی تاریخ کے نتیجے میں کنٹرول کی کمی ہو سکتی ہے۔ ہوسکتا ہے کہ لین دین پہلے ہی ہوچکا ہو یا دستاویزات ابھی ظاہر نہ ہوں۔

لہذا، ہم ایونٹس ڈیٹاسیٹ میں تمام CEO خریداری کی جمع کرانے کی تاریخوں کے ساتھ ساتھ 137 اقساط کو جمع کرتے ہیں جو 20% کی حد سے گزر چکے ہیں۔

purchase_dates = analysis_df[['ticker', 'filed_at']].dropna().drop_duplicates()
purchase_dates['filed_at'] = pd.to_datetime(purchase_dates['filed_at'])
purchase_dates = purchase_dates.rename(columns={'filed_at': 'purchase_date'})
purchase_dates

خریداری کی تاریخ

اب، ہر امیدوار کے لیے، ہمیں پہلے قریب ترین خریداری کی رپورٹ تلاش کرنے کی ضرورت ہے، اس کے بعد قریب ترین خریداری کی رپورٹ۔

دو merge_asof() آپریشنز اس کو سنبھالتے ہیں۔ پہلا پیچھے کی طرف اور دوسرا آگے تلاش کرتا ہے۔

control_candidates = pd.merge_asof(
    control_candidates.sort_values('date'),
    purchase_dates.sort_values('purchase_date'),
    by='ticker',
    left_on='date',
    right_on='purchase_date',
    direction='backward'
)

control_candidates = control_candidates.rename(columns={'purchase_date': 'previous_purchase_date'})

control_candidates = pd.merge_asof(
    control_candidates.sort_values('date'),
    purchase_dates.sort_values('purchase_date'),
    by='ticker',
    left_on='date',
    right_on='purchase_date',
    direction='forward'
)

control_candidates = control_candidates.rename(columns={'purchase_date': 'next_purchase_date'})

اب ہر امیدوار دونوں طرف سے قریبی سی ای او کو جانتا ہے۔

ہم ان دستاویزات سے فاصلے کا حساب لگا سکتے ہیں اور صرف ان تاریخوں کو رکھ سکتے ہیں جو دونوں دستاویزات کے علاوہ 28 دن سے زیادہ ہیں۔

days_from_previous = (control_candidates['date'] - control_candidates['previous_purchase_date']).dt.days
days_to_next = (control_candidates['next_purchase_date'] - control_candidates['date']).dt.days

far_from_previous = control_candidates['previous_purchase_date'].isna() | (days_from_previous > 28)
far_from_next = control_candidates['next_purchase_date'].isna() | (days_to_next > 28)

control_candidates = control_candidates[far_from_previous & far_from_next]
control_candidates

اگر پچھلی یا اگلی دستاویز غائب ہے تو یہ ٹھیک ہے۔ اس کا سیدھا مطلب ہے کہ دستیاب ڈیٹا سیٹ کے اندر امیدواروں کی طرف سے کوئی سی ای او خرید نہیں تھا۔

فلٹر شدہ امیدوار

اس مقام پر، باقی تمام قطاریں تاریخوں کی نمائندگی کرتی ہیں اگر:

  • اسٹاک کی قیمتیں کم از کم 20 فیصد گر گئی

  • وہ تاریخ سی ای او کی خریداری کے قریب نہیں تھی۔

  • ڈرا ڈاؤن کا حساب لگانے کے لیے اسٹاک کے پاس تاریخی قیمت کا کافی ڈیٹا تھا۔

خریداری کی اقساط کو کنٹرول کے ساتھ میچ کریں۔

اب اصل مماثلت ہوتی ہے۔

ہم سب سے پہلے ہر CEO خریداری کے ایپی سوڈ اور ہر کنٹرول امیدوار کے لیے ایک منفرد شناخت کنندہ فراہم کرتے ہیں۔ چونکہ میچز ایک ہی اسٹاک اور سال سے آنے چاہئیں، اس لیے ہم کیلنڈر سال بھی نکالتے ہیں۔

purchase_pool = episodes_20.reset_index(drop = True)

purchase_pool['first_filing_date'] = pd.to_datetime(purchase_pool['first_filing_date'])
purchase_pool['year'] = purchase_pool['first_filing_date'].dt.year
purchase_pool['purchase_id'] = np.arange(len(purchase_pool))

control_candidates['year'] = control_candidates['date'].dt.year
control_candidates['control_id'] = np.arange(len(control_candidates))

مماثلت ہر ٹکر سال کے گروپ میں الگ الگ ہوتی ہے۔

ہم کہتے ہیں کہ جب اسٹاک 32% گر جاتا ہے تو سی ای او خریدتا ہے۔ ہم بائ بیک تاریخوں کی تلاش کرتے ہیں جو اسی اسٹاک اور سال کے لیے 32% ڈرا ڈاؤن کے قریب تھیں، جبکہ تاریخ کے وقفوں کو 180 دنوں کے اندر رکھتے ہوئے.

ایک جوڑا صرف اس صورت میں درست ہے جب:

اگلا بلاک ممکنہ جوڑی بناتا اور استعمال کرتا ہے۔ linear_sum_assignment() ون ٹو ون میچ سیٹ منتخب کریں۔

matches = []
max_drawdown_gap = 0.05
max_calendar_gap = 180

for (ticker, year), purchases in purchase_pool.groupby(['ticker', 'year']):
    controls = control_candidates[(control_candidates['ticker'] == ticker) & (control_candidates['year'] == year)].copy()

    if controls.empty:
        continue

    purchase_drawdowns = purchases['initial_drawdown'].to_numpy()[:, None]
    control_drawdowns = controls['drawdown'].to_numpy()[None, :]
    drawdown_cost = np.abs(purchase_drawdowns - control_drawdowns)

    purchase_dates = purchases['first_filing_date'].to_numpy(dtype="datetime64[D]")
    control_dates = controls['date'].to_numpy(dtype="datetime64[D]")
    calendar_gap = np.abs((purchase_dates[:, None] - control_dates[None, :]).astype('timedelta64[D]').astype(int))

    valid = (drawdown_cost <= max_drawdown_gap) & (calendar_gap <= max_calendar_gap)

    if not valid.any():
        continue

    cost = drawdown_cost + calendar_gap / 1000000
    cost[~valid] = 1000000

    row_indices, column_indices = linear_sum_assignment(cost)
    keep = cost[row_indices, column_indices] < 1000000

    selected = pd.DataFrame({
        'purchase_id': purchases.iloc[row_indices[keep]]['purchase_id'].to_numpy(),
        'control_id': controls.iloc[column_indices[keep]]['control_id'].to_numpy(),
        'drawdown_gap': drawdown_cost[row_indices[keep], column_indices[keep]],
        'calendar_gap_days': calendar_gap[row_indices[keep], column_indices[keep]]
    })

    matches.append(selected)

matched_pairs = pd.concat(matches, ignore_index=True)

مرکزی خیال یہ ہے کہ کوڈ پہلے ظاہر ہونے سے زیادہ آسان ہے۔

drawdown_cost یہ پیمائش کرتا ہے کہ دو زوال کتنے فاصلے پر ہیں۔ سے خریدیں۔ -0.32 اور کنٹرول ہے۔ -0.34 ایک فرق ہے 0.02یا 2 فیصد پوائنٹس۔

کیلنڈر کا فاصلہ بہت چھوٹے ٹائی بریکر کے طور پر شامل کیا جاتا ہے۔ ڈراپ مماثلت ایک اہم ترجیح بنی ہوئی ہے، لیکن قریبی تاریخوں کو ترجیح دی جاتی ہے جب دو کنٹرول تقریباً یکساں قریب ہوں۔

linear_sum_assignment() ایک ہی کنٹرول کو متعدد خریداری کے اقساط پر منتقل ہونے سے روکتا ہے۔ ون ٹو ون میچوں کا ایک سیٹ تلاش کریں جو گروپوں میں مشترکہ لاگت کو کم کرتا ہے۔

حتمی مماثل ڈیٹاسیٹ کی تعمیر

میچ کے نتائج میں صرف موجودہ خریداری ID، کنٹرول ID، اور فاصلے کی پیمائش شامل ہوتی ہے۔

آخری مرحلہ اصل خریداری اور کنٹرول کی تفصیلات کو دوبارہ جوڑے میں ضم کرکے دونوں اطراف کے ریٹرن کا حساب لگانا ہے۔

selected_controls = control_candidates[['control_id', 'ticker', 'date', 'adjusted_close', 'drawdown', 'drawdown_pct']].rename(columns={'ticker': 'control_ticker',
                                                                                                                                       'date': 'control_date',
                                                                                                                                       'adjusted_close': 'control_signal_price',
                                                                                                                                       'drawdown': 'control_drawdown',
                                                                                                                                       'drawdown_pct': 'control_drawdown_pct'})

matched_sample = matched_pairs.merge(purchase_pool,on='purchase_id',how='left').merge(selected_controls,on='control_id',how='left')
matched_sample

ہر قطار میں اب ایک CEO خریداری کا ایپی سوڈ اور متعلقہ خریداری کی منسوخی کی تاریخ شامل ہے۔

حتمی کنٹرول گروپ

اب ہمارے پاس دو گروہ ہیں جو ہم اصل میں شروع سے چاہتے تھے۔ یعنی، ایک گروپ جہاں CEO بڑی کمی کے بعد خریدتا ہے اور ایک گروپ جہاں ایک ہی اسٹاک میں قریبی CEO کی خریداری کے بغیر اسی طرح کی کمی واقع ہوتی ہے۔

سی ای او کی خریداریوں سے اسی طرح کی غیر خریداری کی شرح کا موازنہ کریں۔

یہ وہ جگہ ہے جہاں ورک فلو آخر کار پکڑ لیتا ہے۔

ہر قطار matched_sample ایک ہی اسٹاک کے لیے دو تاریخوں پر مشتمل ہے۔

اس مقام سے، دونوں اطراف کے ساتھ بالکل یکساں سلوک کیا جانا چاہیے۔ فائل کرنے کی تاریخ کے بعد CEO کی طرف پہلے تجارتی دن میں داخل ہوتا ہے۔ کنٹرول سائیڈ میچنگ ڈاؤن ڈے کے بعد پہلے تجارتی دن میں داخل ہوتا ہے۔ دونوں ایک جیسی ایڈجسٹ شدہ قیمتیں اور ایک ہی واپسی کی حد استعمال کرتے ہیں۔

پہلے، آئیے دو سگنل ڈیٹ کالم تیار کریں اور ٹکر سطح کی قیمت کا نقشہ دوبارہ بنائیں جو ہم نے پہلے استعمال کیا تھا۔

matched_sample['first_filing_date'] = pd.to_datetime(matched_sample['first_filing_date'])
matched_sample['control_date'] = pd.to_datetime(matched_sample['control_date'])
historical_df['date'] = pd.to_datetime(historical_df['date'])

prices = historical_df[['ticker', 'date', 'adjusted_close']].dropna().sort_values(['ticker', 'date'])
price_map = {ticker: group.reset_index(drop=True) for ticker, group in prices.groupby('ticker')}

قیمت کا نقشہ ہر ٹکر کے لیے ایک منفرد آرڈر کی تاریخ فراہم کرتا ہے۔ یہ آپ کو ہر گروپ کے لیے الگ الگ منطق لکھنے کے بجائے خریداری کی تاریخوں اور کنٹرول کی تاریخوں دونوں کے لیے واپسی کا ایک فنکشن استعمال کرنے کی اجازت دیتا ہے۔

تمام سگنل کی تاریخوں سے آگے کی واپسی کا حساب لگائیں۔

مندرجہ ذیل فنکشن صرف دو ان پٹ لیتا ہے: ایک ٹکر اور ایک سگنل کی تاریخ۔

اس تاریخ کے بعد پہلا تجارتی سیشن تلاش کریں، انٹری پرائس کے طور پر ایڈجسٹ بند کا استعمال کریں، اور 21، 63، 126، اور 252 تجارتی دنوں کے بعد واپسی کا حساب لگائیں۔

def get_forward_returns(ticker, signal_date):
    result = {
        'entry_date': pd.NaT,
        'entry_price': np.nan,
        'return_1m': np.nan,
        'return_3m': np.nan,
        'return_6m': np.nan,
        'return_12m': np.nan
    }

    ticker_prices = price_map.get(ticker)

    if ticker_prices is None or pd.isna(signal_date):
        return pd.Series(result)

    dates = ticker_prices['date'].to_numpy(dtype="datetime64[ns]")
    entry_index = np.searchsorted(dates, np.datetime64(signal_date), side="right")

    if entry_index >= len(ticker_prices):
        return pd.Series(result)

    entry_price = ticker_prices.loc[entry_index, 'adjusted_close']
    result['entry_date'] = ticker_prices.loc[entry_index, 'date']
    result['entry_price'] = entry_price

    for label, days in {'1m': 21, '3m': 63, '6m': 126, '12m': 252}.items():
        target_index = entry_index + days

        if target_index < len(ticker_prices):
            target_price = ticker_prices.loc[target_index, 'adjusted_close']
            result[f'return_{label}'] = target_price / entry_price - 1

    return pd.Series(result)

اہم تفصیلات یہ ہیں۔ side="right".

یہ دونوں گروہوں کو سگنل کی تاریخ میں داخل ہونے سے روکتا ہے۔ سی ای او کی خریداری کی رپورٹنگ رپورٹنگ کے بعد شروع ہوتی ہے، اور کنٹرول رپورٹنگ مماثل واپسی کی تاریخ کے بعد شروع ہوتی ہے۔

یہ فنکشن تمام آؤٹ پٹ کے لیے گم شدہ اقدار کے ساتھ شروع ہوتا ہے۔ اگر ٹکر دستیاب نہیں ہے یا کسی خاص مدت کے لیے مستقبل کی قیمتوں کی کافی تاریخ نہیں ہے، تو وہ منافع مندرجہ ذیل طور پر برقرار رکھے جاتے ہیں: NaN.

دونوں گروپوں پر ایک ہی واپسی کی منطق کا اطلاق کریں۔

اب ہم تمام مماثل جوڑوں کے لیے دو بار فنکشن چلاتے ہیں۔

پہلا پاس CEO خریداری ٹکر اور جمع کرانے کی تاریخ کا استعمال کرتا ہے۔ دوسرا کنٹرول ٹکر اور کنٹرول کی تاریخ کا استعمال کرتا ہے۔ واپس کیے گئے کالم پہلے سے لگائے گئے ہیں تاکہ آپ آسانی سے دونوں سیٹوں کے درمیان فرق کر سکیں۔

purchase_returns = matched_sample.apply(lambda row: get_forward_returns(row['ticker'], row['first_filing_date']), axis=1).add_prefix('purchase_')
control_returns = matched_sample.apply(lambda row: get_forward_returns(row['control_ticker'], row['control_date']), axis=1).add_prefix('control_')
matched_returns = pd.concat([
    matched_sample.reset_index(drop=True),
    purchase_returns.reset_index(drop=True),
    control_returns.reset_index(drop=True)], axis=1)

نتیجہ خیز ڈیٹا فریم اب دونوں نتائج کو ساتھ ساتھ رکھتا ہے۔

تمام جوڑے تمام افق پر زندہ نہیں رہتے ہیں۔ ایک جوڑا صرف اس صورت میں استعمال کیا جا سکتا ہے جب دونوں فریقین کے پاس مستقبل کی قیمت کی کافی تاریخ ہو۔ یہی وجہ ہے کہ جیسے جیسے 12 مہینے گزرتے ہیں مشاہدات کی تعداد کم ہوتی جاتی ہے۔

حتمی موازنہ بنانا

آخری مرحلہ ہر افق پر دو واپسی سیریز کا موازنہ کرنا ہے۔

ہم حساب کریں گے:

  • ہر گروپ کے لیے اوسط واپسی۔

  • ہر گروپ کے لیے درمیانی واپسی۔

  • مماثل جوڑوں کے اندر اوسط واپسی کا فرق

  • مماثل جوڑوں کے اندر اوسط واپسی کا فرق

  • واپسی کی مثبت شرح

  • جوڑوں کا فیصد جس میں CEO-بائی سائیڈ کنٹرول جیتتی ہے۔

comparison = []

for horizon in ['1m', '3m', '6m', '12m']:
    purchase_col = f'purchase_return_{horizon}'
    control_col = f'control_return_{horizon}'
    valid = matched_returns[[purchase_col, control_col]].dropna()
    differences = valid[purchase_col] - valid[control_col]

    comparison.append({
        'horizon': horizon,
        'matched_pairs': len(valid),
        'purchase_mean': valid[purchase_col].mean(),
        'control_mean': valid[control_col].mean(),
        'mean_difference': differences.mean(),
        'purchase_median': valid[purchase_col].median(),
        'control_median': valid[control_col].median(),
        'median_difference': differences.median(),
        'purchase_positive_rate': (valid[purchase_col] > 0).mean(),
        'control_positive_rate': (valid[control_col] > 0).mean(),
        'purchase_win_rate': (valid[purchase_col] > valid[control_col]).mean()
    })

comparison_df = pd.DataFrame(comparison)
comparison_df

جوڑے کے حساب سے اعدادوشمار یہاں اہم ہیں۔

median_difference یہ اس کا میڈین ہے:

CEO-خریداری آمدنی سے مماثل کنٹرول آمدنی

ہر جوڑے کے لیے۔ یہ صرف میڈین سی ای او مائنس میڈین کنٹرول نہیں ہے۔

مشکلات بہت زیادہ براہ راست سوال پوچھتی ہیں۔ CEO خریدنے والے ایپی سوڈ میں سے کتنے فیصد مماثل جوڑوں نے اصل میں بہتر کارکردگی کا مظاہرہ کیا؟

حتمی موازنہ

ایک ماہ کی کارکردگی کمزور رہی۔ CEO خریدنے والے ایپی سوڈز اوسط، درمیانی جوڑی کے فرق، مثبت واپسی، اور جیت کی شرحوں کے لیے کنٹرول سے پیچھے رہ جاتے ہیں۔

تین ماہ واحد مدت تھی جس میں نتائج پورے بورڈ میں یکساں رہے۔ سی ای او پرچیزنگ گروپ نے اوسطاً 6.2 فیصد پوائنٹس زیادہ حاصل کیے، 3.9 پوائنٹس کا مثبت میڈین جوڑا فائدہ حاصل کیا، اور جیت کی شرح 59.6% تھی۔

6 ماہ اور 12 ماہ کی اوسط عام جوڑی سے زیادہ مضبوط لگ رہی تھی۔ 12 ماہ کے بعد، CEO گروپ نے اوسطاً تقریباً 10 فیصد پوائنٹس زیادہ حاصل کیے، لیکن مقابلے میں صرف 37.9 فیصد وقت پر بیٹ کنٹرول کرتا ہے۔

اس مجموعہ کا عام طور پر مطلب یہ ہوتا ہے کہ چند بڑے فاتح اوسط کو اوپر کی طرف دھکیلتے ہیں۔

تو حتمی جواب یہ نہیں ہے کہ سی ای او کی خریداریوں نے ہمیشہ کام کیا یا ان کی کبھی اہمیت نہیں رہی۔ ظاہری کنارہ افق پر بہت زیادہ منحصر تھا، اور تین ماہ واحد مدت تھی جس میں مختلف پیمائشیں ایک ہی سمت کی طرف اشارہ کرتی تھیں۔

جو ہم نے اپنے کیس اسٹڈی میں پایا

خام تعداد نے CEO کی خریداری کو مجموعی طور پر پرامید بنا دیا۔ 12 ماہ کے بعد اوسط واپسی 35.4% تھی، دستیاب مشاہدات میں سے تقریباً دو تہائی مثبت تھے۔

لیکن بغیر خرید کے نقصانات کو ملانے کے بعد، کہانی بہت تنگ ہو جاتی ہے۔

  • ایک ماہ نے کوئی فائدہ نہیں دکھایا۔ CEO خریدنے والے ایپی سوڈز کا اوسط، درمیانی جوڑی کے فرق، مثبت منافع، اور جیت کی شرحوں پر ناقص کنٹرول تھا۔

  • تین مہینے سب سے مضبوط دور تھے۔ CEO گروپ نے اوسطاً 6.2 فیصد پوائنٹس زیادہ حاصل کیے اور اپنے مماثل کنٹرولز کو 59.6% جوڑوں میں شکست دی۔ یہ واحد افق تھا جہاں کلیدی اشارے ایک ہی سمت میں اشارہ کرتے تھے۔

  • چھ اور بارہ مہینے بھروسہ کرنا مشکل تھا۔ اگرچہ سی ای او پرچیزنگ گروپ کے لیے اوسط زیادہ تھی، جوڑی کے لیے درمیانی فرق منفی تھا اور زیادہ تر انفرادی اقساط قابو سے باہر تھیں۔

  • قرعہ اندازی نے خود ہی بہت کچھ سمجھا دیا۔ جن اسٹاکس میں کمی واقع ہوئی تھی وہ اکثر CEO کی خریداری کے بغیر واپس لوٹ جاتے ہیں، لہذا خریداریوں کے بعد خالص واپسی نے اشارہ کو بڑھا چڑھا کر پیش کیا۔

سب سے زیادہ قابل دفاع نتیجہ یہ نہیں ہے کہ سی ای او کی خریداری طویل مدتی بحالی کی پیش گوئی کرتی ہے۔ اس نمونے میں یہ 3 ماہ کے ریورسل سگنل کے قریب ہے اور یہاں تک کہ نتیجہ کو تجارتی اصول کے بجائے ایک ریسرچ سگنل سمجھا جانا چاہیے۔

یہ ٹیسٹ آپ کو کیا بتا سکتا ہے اور کیا نہیں بتا سکتا

یہ 500 اسٹاک کا اسکرینر پر مبنی نمونہ تھا، پوری مارکیٹ کا نہیں۔ خلا میں بقا کا تعصب ہوسکتا ہے۔ کچھ کوڈ ہے۔P ہو سکتا ہے کہ خریداریاں مکمل طور پر صوابدیدی نہ ہوں، اور اسی طرح کے ڈرا ڈاؤن سے مماثل ہونا یہ ثابت نہیں کرتا ہے کہ CEO کی خریداریوں سے منافع ہوا ہے۔ یہ ایک ریسرچ کیس اسٹڈی ہے، تجارتی حکمت عملی نہیں۔

ورک فلو کا سب سے کارآمد حصہ اندرونی سگنلز کو ان اسٹاکس سے الگ کرنا تھا جو پہلے ہی اپنے طور پر نیچے تھے۔ اس سے پہلے کہ ہم کنٹرولز کو شامل کریں، سی ای او کی خریداری بہت سے محاذوں پر عام طور پر پرامید نظر آتی تھی۔ اس کو شامل کرنے کے بعد، نتائج بہت کم ہو گئے۔ تین ماہ کا فائدہ ممکن ہے، لیکن کوئی صاف طویل مدتی گارنٹی نہیں ہے۔

یہ ثابت کرنے کے مقابلے میں کم دلچسپ لگ سکتا ہے کہ سی ای او کی خریداری بحالی کی پیش گوئی کرتی ہے۔ لیکن یہ ایک بہتر جواب ہے۔ ورک فلو نے ہمیں اس کہانی کی جانچ کرنے پر مجبور کیا جس پر ہم ایک بیس لائن کے خلاف یقین کرنا چاہتے تھے، اور بیس لائن نے ہمارے نتائج کو بدل دیا۔

اوپر تک سکرول کریں۔