Вы думали, что PowerShell — это только для администрирования серверов? Вовсе нет! Сегодня мы займёмся благородным делом: подсчётом чужих денег. И не где-нибудь, а в официальных документах Комиссии по ценным бумагам и биржам США (SEC). Всё это — из консоли, с каплей Vega и щепоткой аналитического озорства.
Объект нашего любопытства: форма 4, где корпоративные руководители отчитываются о своих сделках с акциями:
- Продал что-то? Отчитайся.
- Подарил акции супруге? Всё равно отчитайся.
- Получил бонус акциями? Даже если просто «спасибо» — отчитайся!
Почему нам это интересно? Ну… просто интересно, кто слил акции перед падением цены 😉
Получаем данные
Для этого нам понадобятся две функции PowerShell:
- Get-RecentSecForm4XmlUrls — наш следопыт, просматривает архивы SEC и достаёт URL XML-документов.
- Convert-Form4XmlToRecord — парсит XML и превращает его в объект PowerShell. Потому что читать сырой XML — боль. Пусть скрипт страдает.
function Get-RecentSecForm4XmlUrls {
param (
[string]$CIK = "0000789019",
[int]$DaysBack = 100
)
$headers = @{
"User-Agent" = "PowerShellScript/1.0 (eosfor@gmail.com)"
"Accept-Encoding" = "gzip, deflate"
}
$url = "https://data.sec.gov/submissions/CIK$CIK.json"
$data = Invoke-RestMethod -Uri $url -Headers $headers
$cikTrimmed = $CIK.TrimStart("0")
$cutoffDate = (Get-Date).AddDays(-$DaysBack)
$results = @()
for ($i = 0; $i -lt $data.filings.recent.form.Length; $i++) {
$formType = $data.filings.recent.form[$i]
if ($formType -ne "4") { continue }
$filingDate = Get-Date $data.filings.recent.filingDate[$i]
if ($filingDate -lt $cutoffDate) { continue }
$accessionNumber = $data.filings.recent.accessionNumber[$i]
$primaryDoc = $data.filings.recent.primaryDocument[$i]
$reportDate = $data.filings.recent.reportDate[$i]
$folder = $accessionNumber -replace "-", ""
$xmlFileName = [System.IO.Path]::GetFileNameWithoutExtension($primaryDoc) + ".xml"
$xmlUrl = "https://www.sec.gov/Archives/edgar/data/$cikTrimmed/$folder/$xmlFileName"
$results += [PSCustomObject]@{
FilingDate = $filingDate.ToString("yyyy-MM-dd")
ReportDate = $reportDate
XmlUrl = $xmlUrl
}
}
return $results
}
function Convert-Form4XmlToRecord {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[pscustomobject]$InputObject
)
process {
$headers = @{
"User-Agent" = "PowerShellScript/1.0 (eosfor@gmail.com)"
}
try {
[xml]$doc = Invoke-WebRequest -Uri $InputObject.XmlUrl -Headers $headers -UseBasicParsing
}
catch {
Write-Warning "Download failed: $($InputObject.XmlUrl)"
return
}
$issuer = $doc.ownershipDocument.issuer.issuerName
$owner = $doc.ownershipDocument.reportingOwner.reportingOwnerId.rptOwnerName
$ownerRelationship = $doc.ownershipDocument.reportingOwner.reportingOwnerRelationship
# Get all role flags where value is '1'
$relationshipProps = ($ownerRelationship | Get-Member -MemberType Properties | Where-Object {
$ownerRelationship.$($_.Name) -eq "1"
}).Name
# Join multiple roles if needed
$relationship = if ($relationshipProps.Count -gt 1) {
$relationshipProps -join ";"
} else {
$relationshipProps
}
# Собираем footnotes в хештаблицу
$footnotes = @{}
if ($doc.ownershipDocument.footnotes -and $doc.ownershipDocument.footnotes.footnote) {
$rawFootnotes = $doc.ownershipDocument.footnotes.footnote
if ($rawFootnotes -is [System.Array]) {
foreach ($f in $rawFootnotes) {
$footnotes[$f.id] = $f.'#text' ?? $f.InnerText
}
} else {
$footnotes[$rawFootnotes.id] = $rawFootnotes.'#text' ?? $rawFootnotes.InnerText
}
}
$transactions = $doc.ownershipDocument.nonDerivativeTable.nonDerivativeTransaction
foreach ($txn in $transactions) {
$note = $null
if ($txn.footnoteId) {
$ids = if ($txn.footnoteId -is [System.Array]) {
$txn.footnoteId | ForEach-Object { $_.id }
} else {
@($txn.footnoteId.id)
}
$note = ($ids | ForEach-Object { $footnotes[$_] }) -join "; "
}
[PSCustomObject]@{
FilingDate = $InputObject.FilingDate
ReportDate = $InputObject.ReportDate
Issuer = $issuer
InsiderName = $owner
InsiderRole = $relationship
SecurityTitle = $txn.securityTitle.value
TransactionDate = $txn.transactionDate.value
TransactionCode = $txn.transactionCoding.transactionCode
SharesTransacted = $txn.transactionAmounts.transactionShares.value
PricePerShare = $txn.transactionAmounts.transactionPricePerShare.value
SharesOwnedAfterTxn = $txn.postTransactionAmounts.sharesOwnedFollowingTransaction.value
OwnershipType = $txn.ownershipNature.directOrIndirectOwnership.value
IndirectOwnershipNature = $txn.ownershipNature.natureOfOwnership.value
Footnote = $note
XmlUrl = $InputObject.XmlUrl
}
}
}
}
📥 Запустим наш скрипт наблюдения и сохраним данные в переменной $allData. Что-то вроде «проверки биографии», но легально.
$CIKs = "0000789019", "0000320193", "0001318605", "0001288776", "0001352010"
$allData = $CIKs | % { Get-RecentSecForm4XmlUrls -CIK $_ -DaysBack ((Get-Date).DayOfYear) } | Convert-Form4XmlToRecord
🧹 Следующий шаг — навести порядок. Нас интересуют только транзакции, в которых действительно двигались деньги. Если количество акций — 0, пропускаем. Нас интересуют настоящие миллионные сделки (ну или хотя бы что-то приличное).
$data = $allData |
Select-Object TransactionDate, SharesTransacted, TransactionCode |
Where-Object { $_.TransactionCode -in @("S", "P", "F", "A", "M", "G") -and $_.SharesTransacted -gt 0 }
$data = $data | ForEach-Object {
$action = switch ($_.TransactionCode) {
"S" { "Sell"; break }
"F" { "Sell"; break }
"G" { "Sell"; break }
"A" { "Buy"; break }
"P" { "Buy"; break }
"M" { "Buy"; break }
default { "Other" }
}
$_ | Add-Member -NotePropertyName Action -NotePropertyValue $action -Force -PassThru
}
🔧 Чуть не забыли! Чтобы всё это заработало, мы немного доработали dotnet/interactive. Почему? Потому что параметр CustomMimeType в Out-Display… ну, он как бы был, но не совсем работал. Теперь работает — JSON-спеки прямо из ячейки ноутбука, красивые графики. Благодарности можно отправлять автору PR #3671 — то есть мне 😉
Диаграмма рассеяния
Добавим немного интерактива к странице 🙂
📈 Диаграмма рассеяния — визуальный допрос:
- X — дата транзакции
- Y — количество акций
- Цвет — зелёный (покупка) или красный (продажа)
- Подсказка — кто, когда, сколько и код SEC
Тепловая карта
🔥 Тепловая карта — следи за жаром:
- X — тип транзакции
- Y — инсайдер
- Цвет — зелёный, если есть данные о сумме, серый — если нет
- Подсказка — сколько сделок, акций и общая сумма
🔍 TransactionCode — расшифровка
| Code | Что значит | Как понимать |
|---|---|---|
| A | Награда | Акции в подарок, обычно бонус. Как подарочная карта, только акциями |
| S | Продажа | Продажа акций. Иногда массовая. Часто — перед падением цен |
| F | Налоги | Удержание акций для уплаты налогов. Хоть не себе оставили |
| M | Опционы | Исполнение опциона. Купить дёшево — продать дорого |
| G | Подарок | Подарено. Семье. Трасту. Благотворительности |
| P | Покупка | Купили акции на свои. Респект |
| I | Плановая | Автоматическая сделка по плану. Легально? Смотря кто спросит |
| C | Конверсия | Преобразование деривативов в обычные акции |
Пузырьковая диаграмма
🔵 Пузырьковая диаграмма — каждый пузырь — сделка, размер — объём. Чем больше пузырь — тем сочнее сделка:
- X — дата
- Y — кто
- Размер — количество акций
- Цвет — тип сделки
- Подсказка — все грязные подробности