【読書メモ】「市場サイクルを極める」
まとめ
市場の将来は誰しも予測できないが、サイクルが無くなることは決してない。サイクルが存在する根本的な原因は、人がかかわっていることにあるからである。サイクルは投資家の揺れ動く過剰に行き過ぎた心理によって形成されるものであり、合理性の仮定からかけ離れた行動の結果である。
最大の投資リスクは、経済関連のマイナス材料や予測を下回る企業情報ではなく、市場の懐疑心が薄れ、リスク許容と楽観主義に満たされているとき、投資家は妥当なリスクプレミアムに固執せず、資産価格が過度に高い水準に達した時に訪れる。
このように行き過ぎたリスク許容の姿勢は危険の発生を後押しするが、一方でリスク回避姿勢が行き過ぎたところまで振り子が振れると絶好の買い場をもたらす。過去のテックバブル、金融危機など幾つものバブル形成と崩壊が繰り返されてきた。
物事の理由や結果が過去の例と全く同じになる事はあり得ないが、たいていの場合、かつて見たのと似たような展開をたどる。つまり「歴史は繰り返さないが、韻を踏む」のだ。従って、将来を見通すことが出来なくとも、サイクルのどの位置にあり、それが将来の動向にどう影響するかを理解する姿勢は極めて重要である。
その為にカギとなるのは心理の振り子とバリュエーションサイクルが今どの状態にあるかを知る事だ。過度に楽観的な心理と、高すぎるバリュエーションを積極的に受け入れる姿勢から価格がピークに近い水準まで高騰しているときには買わない、
そして、冷え込んだ心理とバリュエーションの低下でパニックに陥った投資家が全般的に価格が低下しているにもかかわらず、売りに走っている時に買うべきだ。従って、ポートフォリオ構築においてもサイクルの中での位置づけに基づいたポジショニングをとり、攻撃的ないしは守備的に構築する。
投資家はバリュエーションの著しい乖離を正当化する術に長けており、「今回は違う」とサイクルの存在を無視し、これまでのトレンドをそのまま未来に当てはめようとすることがあるが、これは投資家が冒しうる重大な危険の一つであり、危機の予兆となる。
繰り返しになるが、極端な行動をとる人間の性向が無くなることはなく、そうした極端な状態はいずれ修正されるため、サイクルが無くなることはないだろう。
2008年金融危機に見る市場の振り子
証券市場における地合いの動きは、振り子の振動によく似ている。
これは概して投資家の行き過ぎた心理状態(楽観主義、強欲、リスク許容 ⇔ 悲観主義、恐怖、リスク回避)によって起こる。
例えば2008年の金融危機の住宅ローン担保証券が商品化された当初は様々な背景が住宅価格を後押しした。
<楽観主義、強欲、リスク許容を強めた背景>
・家を持つというアメリカンドリームを広めることに熱心な政治家による煽り(ジョージ・W・ブッシュ)
・FRBによる全般的な利下げと高利回り商品に対する旺盛な需要
・中国や原油生産国など国外へ流れた過剰流動性が還流するという確信
・住宅ローンを原資産とした新たな派生証券の登場とニーズの高まり
・住宅ローン貸し手の緩過ぎる融資基準、格付機関の利益競争を背景とした不当な格付けのかさ増し
・投資銀行によるリスクを度外視した仕組債の発行
これらの要因が一層、商品価格を押し上げ、市場は寛容なリスク許容姿勢と楽観主義を強めていった。
しかしご存知の通り、サブプライムローンの借り手によるデフォルトが大々的に発生した結果、住宅ローン担保証券の相場は激しい下方スパイラルに陥った。
<悲観主義、恐怖、リスク回避を強めた背景>
・甘い融資姿勢が明らかになり、融資基準の緩和でデフォルト率が前代未聞の水準に達する可能性が全く織り込まれていなかったことが露呈した
・レバレッジを利かせた住宅ローン担保証券が融資の契約条項を満たせなくなり、発行体の債務不履行が発生した
・金融機関の株式が容赦なく売りたたかれ、規制緩和により銀行がレバレッジ比率を高めることが出来るようになった結果、複数の銀行の存続が困難となった
悲観論が悲観論を呼び、一時は高すぎるバリュエーションを度外視する程の熱狂に駆られていた投資家が今度は安すぎるバリエーションでも手を出さなかった。
行き過ぎたリスク許容の姿勢は危険の発生を後押しするが、リスク回避姿勢が行き過ぎたところまで振り子が振れると絶好の買い場をもたらす。
(著者のファンドは相場が急落するさなかディストレスト・デット投資により巨額の利益を上げている)
客観性と理性を持ち合わせた中立的で安定した姿勢を保てる投資家はほとんどいないため、心理のサイクルは概してどちらかの行き過ぎた状態にあり、中心点で費やす時間はほとんどない。
歴史は繰り返さないが、韻を踏む
投資の世界ではサイクルが浮き沈みを繰り返し、振り子は行きつ戻りつしている。
投資家の行き過ぎた心理により形成されるサイクルは、形は違えど過去にもみられる。(1960年代のニフティ・フィフティブームや2000年にかけてのハイテク・バブルなど)
「歴史は繰り返さないが、韻を踏む」 by マーク・トウェイン
細かい点はさまざまに異なるが、こうしたパターンの確実性を認識し、現在がサイクルのどの位置にあり、それが将来の動向にどう影響するかを理解する姿勢は非常に重要である。
心理の振り子とバリュエーションサイクル
サイクルの現在地を推測するためにカギとなるのが心理の振り子とバリュエーションサイクルである。過度に楽観的な心理と、高すぎるバリュエーションを積極的に受け入れる姿勢から価格がピークに近い水準まで高騰しているときには買わない、
そして、冷え込んだ心理とバリュエーションの低下でパニックに陥った投資家が全般的に価格が低下しているにもかかわらず、売りに走っている時に買うべきだ。
その為に周りを見渡して、自問するべきだ。
・投資家は楽観的か、悲観的か?
・新手の投資商品はすんなり受け入れたか、新株発行やファンドの新設に積極的かどうか?
・イールドスプレッドは小幅か大幅か?
・株価収益率(PER)は歴史的にみて高いか低いか?
こうして様々な尺度から現状に目を凝らし、そこから何をすべきか、答えが浮かび上がってくるのを待つ姿勢が重要であり、そうすれば卓越した投資判断を下すことも可能となる。
【Go】学習メモ3:データベース操作(CREATE TABLE, INSERT, UPDATE, データ取得)
SQLiteを使用した簡単なDB操作について
サンプルとして
personという名前のテーブル(name:string, age:int)を作成して、
UPDATE, INSERT,データ取得を実行していきます。
インポートするパッケージ
sqlite を使用するために必要なパッケージのインポート
"github.com/mattn/go-sqlite3"は直接コード内には登場しないですが、ビルドしてコンパイルしないとsqliteが使用できません。
package main import ( "database/sql" "log" _ "github.com/mattn/go-sqlite3" )
func main内で使用する変数とテーブルのstructです。
ここのPersonというstructは後ほど作成するpersonテーブルからデータを取得する際に使います。
var DbConnection *sql.DB type Person struct { Name string Age int }
DBConnectionを用意します。sqlite3:ドライバ
最後にDBを閉じるのでdefer でクローズ
func main() { DbConnection, _ := sql.Open("sqlite3", "./example.sql") defer DbConnection.Close()
CREATE コマンドとコマンドの実行
テーブル作成などの実行コマンドはExecで実行されます。
cmd := `CREATE TABLE IF NOT EXISTS person( name STRING, age INT)` _, err := DbConnection.Exec(cmd) if err != nil { log.Fatalln(err) }
INSERT
INSERTの際はコマンドを記述する際に"?"を入れておき、
Exec内で"?"に対する値を入れることが出来ます。
SQLインジェクションの対策としてこのような書き方が勧められているそうです。
ただし、テーブル名を"?"で入力することはまだ対応されていないそうです。
cmd = "INSERT INTO person (name, age) VALUES (?, ?)" _, err = DbConnection.Exec(cmd, "Mike", 24) if err != nil { log.Fatalln(err) }
UPDATE
UPDATEも同様に"?"による記述とExec実行可能です。
Exec内でUPDATEしたい値を入力します。
cmd = "UPDATE person SET age = ? WHERE name = ?" _, err = DbConnection.Exec(cmd, 25, "Mike") if err != nil { log.Fatalln(err) }
データ取得(マルチセレクト)
テーブルのデータを複数連続で取得したい場合は、マルチセレクトを用います。
DbConnection.Queryを使用しrowsにSQLコマンドを渡します。
予め ppとして定義したPersonのstructを用意しておきます。
cmd = "SELECT * FROM person" rows, _ := DbConnection.Query(cmd) defer rows.Close() var pp []Person
rows.Next()でレコードごとにループで回します。
pをPersonのstructとして定義、
rows.Scan()にpのstructのアドレスを入れておくと値が格納されます。
for rows.Next() { var p Person err := rows.Scan(&p.Name, &p.Age) if err != nil { log.Println(err) } pp = append(pp, p) } if err != nil { log.Fatalln(err) }
データ取得(シングルセレクト)
ある条件の値のみほしい場合はシングルセレクトで取得します。
シングルセレクトではコマンド内にINSERT文と同じように"?"を入力しておき
DbConnection.QueryRow()に検索値(この場合はage = 20が条件)を渡すことでレコードが取得できます。
エラーハンドリング内のsql.ErrNoRowsでは、レコードが存在しない場合はlog.Printlnで"No row"が出力されるようになっています。
cmd = "SELECT * FROM person where age = ?" row := DbConnection.QueryRow(cmd, 20) var p Person err = row.Scan(&p.Name, &p.Age) if err != nil { if err == sql.ErrNoRows { log.Println("No row") } else { log.Println(err) } } fmt.Println(p.Name, p.Age)
【Go】学習メモ2:goroutine(ProducerとConsumer)
goroutineのproducerとconsumerについて少し悩んだのでメモします。
間違い等ありましたらご指摘いただけると幸いです。
1 全体の流れ
ProducerとConsumerはそれぞれgoroutineを内包し、channelを介してタスクの生成と処理を受け渡す流れになります。次のようなイメージです。Producer内で記述されたタスクに従い、channelに処理結果が格納され、さらにConsumer内の記述によってchannelから結果を取り出し処理を施します。
func main()とgoroutineであるProducer または Consumerは各々処理が分離されており、何もしないとgoroutineが完了する前にfunc main()が終了してしまう問題があります。
そうならないようfunc main()とgoroutineの連携の為には、sync.WaitGroupを宣言することで、func main()はgoroutineの処理が完了(wg.Done())するまで待機(wg.Wait())します。
繰り返しになりますが、もしこの連携がない場合は双方が分離されたまま処理が進むのでfunc main()が先に終了しgoroutineのプロセスが完了しないままプログラムが終了します。
2 channelのclose
Producer()側でchannelに処理結果を複数送信し、Consumer()側でchannelを繰り返し取り出し処理を施す場合はfor文のRangeが使えます。
しかし、最終的にchannelをclose()で閉じないとchannelはまだ格納できる受信状態にあるため、Rangeによる終わりが定まらず繰り返し処理が正しく機能しません。
次のイメージの通りです。従って、Consumer()内のchannelから結果を取り出す処理を完了させるにはchannelのclose()が必要です。
3 サンプルコードによるポイントの説明
具体的にConsumerとProducerのサンプルコードで説明します。
処理のポイントを〇数字でつけてみました。func main()によって処理が進められていきます。
①:wg.Add(1)によってループ回数ごとに並列化処理数が追加されproducer()が実行されます。
②:producer()内では記述された処理の結果がchannelに送信されます。まだchannelがクローズされていないので、channelは受信待ちの状態のままです。
③:続いてfunc main()からconsumer()が呼ばれます。consumer内のgoroutineが終了するまで間はwg.wait()によってfunc main()は待機。
そして、wg.Done()によってgoroutineの処理の完了がfunc main()へと伝わり、func main()が処理を再開します。
ここが重要なポイントですが、まだchannelは受信待ちで開いた状態の為、rangeの範囲が定まっていないのでfor文自体は処理が完了していません。
④:ここでclose(ch)によってchannelがクローズされたことで、channelに格納されている処理結果の数が確定し、for文も終了となります。
⑤:func main()に記述したtime.Sleep()がないとConsumer()の最後に記述したfmt.Println()が実行されません。何故なら、func main()とgoroutineであるconsumerは処理が分離されており、func main()が先に終了し、プログラムが完了してしまうため、consumer側のfmt.Println()が実行されないからです。そこで、func main()側でtime.Sleepを実行することでconsumer()のgoroutine後の処理を行う猶予を与えています。イメージは次の通りです。
4 使ったサンプルコード
最後にこれまで説明したサンプルコードを載せておきます。
今回は以上になります。ありがとうございました。
package main import ( "fmt" "sync" "time" ) func producer(ch chan int, i int) { // Something ch <- i * 2 } func consumer(ch chan int, wg *sync.WaitGroup) { for i := range ch { fmt.Println("process", i*1000) wg.Done() } fmt.Println("consumer:end") } func main() { var wg sync.WaitGroup ch := make(chan int) // Producer for i := 0; i < 10; i++ { wg.Add(1) go producer(ch, 1) } // Consumer go consumer(ch, &wg) wg.Wait() close(ch) time.Sleep(2 * time.Second) }
【Go】学習メモ1:インターフェースとダックタイピング
アプリケーションを作りたくてGo言語を学んでいます。
所々躓いた点は自分なりに理解した(と思われる)内容やイメージを残していきたいと思います。
如何せん初心者なもので、もし理解が誤っていたら教えていただけると幸いです・・・!
インターフェースのイメージ
インターフェースは次のようなイメージを持ってます。
上から、人類→個人→メソッドといったイメージです。
【インターフェース】
Humanは「喋る」、「歩く」、「寝る」などのメソッドを持っています。(ここではまだ「喋る」や「歩く」がどのような動作なのかは分からない)
【ストラクト】
Personは構造体を表します。(ここでは個人の宣言と名前や年齢などの属性を用意しています。歩くや寝るの動作もあるので、ストレス状況や筋力などの属性も用意してもいいと思います!)
【メソッド】
先ほどの通りHumanに属するPersonはインターフェースにて宣言した「喋る」や「歩く」が出来ます。ここでは『喋るとは?歩くとは?』どのような動作を意味するのかを記述します。
つまり、
①予めインターフェース内にて人型が可能な動作を宣言しておき、
②人型に属する個人が ③インターフェース内で定義した動作をメソッドとして用意する。
そうすることで、人型に割り当てた個人は、いつでもインターフェース内で割り当てた動作を呼び出すことが可能となる。
そんな便利さを持ち合わせているのがインターフェース。でいいのでしょうか(汗
先ほどの図を別の観点からイメージすると下のようになります。
このように、Person(struct)をHuman(Interface)に代入することで、HumanであるPersonはPreson.Say()が実行可能となります。
ある個人は"人型"であり、"人型"は喋るので、ある個人は"喋れる"ということです。
またHuman(Interface)にSleep()などほかのメソッドも宣言したら同様にPersonで使える関数として定義しなければなりません。
実装
これから上のイメージの通り実装していきます。
①予めインターフェース内にて人型が可能な動作(今回はSay()のみ)を宣言しておき、
②人型に属する個人(属性は名前のみ)が ③行えるインターフェース内で定義した動作をメソッドとして用意する。
の順番で進めます。
①予めインターフェース内にて人型が可能な動作(今回はSay()のみ)を宣言しておき、
package main import "fmt" type Human interface { Say() }
②人型に属する個人(属性は名前のみ)が
type Person struct { Name string }
③行えるインターフェース内で定義した動作をメソッドとして用意する。
func (p Person) Say() {
fmt.Println(p.Name)
}
あとはmain関数内で、Humanにmikeという名前のPersonを代入してsayを実行する。
func main() { var mike Human = Person{"mike"} mike.Say() //"mike" }
ダックタイピング
"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)
ja.wikipedia.org
つまり、
"Person("mike")がインターフェースHumanで定義したSay()の関数を持ち合わせているのであれば、PersonもインターフェースHumanを満たしている"ということです。(恐らく・・・)
分かりにくいので例えば、mikeがレンタカーで車を予約し、"Mr.mike"なら乗車可能、それ以外なら乗車不可といったメッセージがアウトプットされるコメントを出力したいとします。
この場合、Person.Say()には戻り値を持たせたいので
①のインターフェースのメソッドSay()の戻り値を文字型に
type Human interface { Say() string }
③のSay()には戻り値を、頭に"Mr."をつけるため、Personの中身を変更するのでポインタレシーバー(*Person)に変更
func (p *Person) Say() string { p.Name = "Mr." + p.Name return p.Name }
新たにDriveCarという関数を定義。
func DriveCar(human Human) { if human.Say() == "Mr.mike" { fmt.Println("ok") } else { fmt.Println("get out!") } }
mikeという構造を持った人型(Human)のPerson(個人)がSay()のメソッドを持ち合わせているため、
DriveCar関数が実行することができました。
先ほどSay()をポインタレシーバとしたので、Person{"mike"}にも&をつけます。
func main() { var mike Human = &Person{"mike"} var x Human = &Person{"x"} DriveCar(mike) // "ok" DriveCar(x) // "get out!" }
今回は以上になります。
【論文メモ】インターネット掲示板情報と株式市場 NO1~4
今回は論文メモです。
テーマはインターネット掲示板情報を用いた株式市場との関係です。メモ書き程度です。
- インターネット株式掲示板の投稿内容と株式市場の関連性
https://sigfin.org/?plugin=attach&refer=SIG-FIN-002-09&openfile=SIG-FIN-002-009.pdf
- インターネット株式掲示板の投稿内容分析に基づくファクターモデル構築の可能性
https://www.jstage.jst.go.jp/article/tjsai/27/6/27_376/_pdf
- 時系列トピックモデルを用いた株式市場の分析
https://sigfin.org/?plugin=attach&refer=SIG-FIN-010-01&openfile=SIG-FIN-010-01.pdf
https://sigfin.org/?plugin=attach&refer=022-07&openfile=SIG-FIN-022-07.pdf
(詳細は拡大してご覧ください)
【ETF分析 ② 前処理編】R Nested Data Frameを活用してCSV Fileの前処理を行う
今回は
の続きデータ前処理編になります。
k-bind.hatenablog.com
Rのtidyverseが提供するNested Data Frameという構造を活用して階層的な処理でCSVファイルのクレンジングを行います。
CSVファイルを眺めて問題点を確認する
まず、こちらは前回スクレイピングしたETFデータ一覧です。全て月次断面のファイルなのでそれぞれ上場している期間に応じて×月数分のファイルが存在します。
それぞれのCSVファイルを開いて見てみると問題点がハッキリとします。
新興国株式ETFと日本株最小分散ETFのCSVファイルを開いてみます。
不要なデータは灰色線で囲み、必要なデータは青線で囲んでいます。
(今回は月次断面のポートフォリオを必要としています)
上の画像の通り、ダウンロードしたETFの銘柄によってファイルフォーマットがまちまちです。取得または提供されたファイルフォーマットがファイル毎にバラバラなのはデータ分析においてはよくある話ですね。
一つ一つファイルフォーマットを確認してデータを加工してもよいのですが銘柄は12種類もあるうえこれ以上増えたりフォーマットが変わったりしたら目で追っていくのは大変です。
そこである程度フォーマットが変わったところで自動的に必要なデータが抽出できるような仕組みを考えたいと思います。
必要なデータのみ取り出すには
ファイルのフォーマットはそれぞれ違えど、共通点があります。
それは記載されている内容がデータサマリーと詳細に分類出来るという事です。
ここでいうデータサマリーは基準日や純資産総額のデータ、詳細は保有銘柄リストに該当します。
データ個数は詳細>>>サマリーと概ねなるので、データの塊毎に個数(面積)を取得し面積が最も大きいデータ集合体を取得することが出来ればつまり保有銘柄一覧を取得で生きると概ね考えられます。
従ってこれからの作業の方向性は下記の通りになります。
1:読み込んだCSVファイルから必要な構成銘柄一覧の情報がどこに存在するか特定する関数を定義
2:月次CSVデータを1で定義した関数で加工処理
3:加工済みデータをマージしてETF銘柄ごとのCSVファイルとして保存する
ざっくりとしたNested Data Frameの説明
今回はRのNested Data Frameを活用して作業していきますのでざっくりとした説明をしたいと思います。Nested Data Frameは今回も含め実に多様な場面で重宝する手段になります。
こちらの図は簡略化したイメージ図になります(厳密的に正確ではないのはご了承ください)。Nested Data Frameは一つのデータフレームの中に階層構造を持たすことが出来ます。
今回は読み込んだCSVのデータ(データフレーム)を階層構造としてデータフレーム内に持たせます。なおネストさせる構造体はデータフレームだけではなく、行列やモデルオブジェクトなども階層的に持たすことが出来ます。
階層構造のデータフレームが持てることの恩恵は色々あるとは思いますが一つはやはりdplyrとして一貫した操作性が保てることだと個人的に思います。
今回は階層化された読み込み済みのCSVデータに対して、必要なデータのみ抽出するオリジナル関数を適応しますが、それ以外にも自身で設計したモデルを適応したりかなり自由かつ柔軟に一つのデータフレーム内で取り扱うことが出来ます。素晴らしいですね!
これから記述するコードのプロセス図は以下の通りになります。
Rコード
ではこれらのプロセスをRで記述していきます。
ライブラリの読み込み(必要に応じてインストールしてください)
library(tidyverse)
前回ダウンロードしたCSVファイル一覧からファイル名をベクトルとして取得する。
csv_path <- "C:/(CSVファイルが保存されているパス)/" # ファイル一覧 csv_files <- list.files(csv_path, pattern = "*csv")
ファイル一覧のベクトルからETF種類を文字列操作で取り出す
# ETF種類一覧 cate_etf <- csv_files %>% str_sub(., 10, 1000) %>% # 左文字10文字目から最後まで取得 str_replace_all(".csv", "") # .csvを取り除く
ここで今回の肝の一つである、読み込んだデータから必要な部分を抽出し抽出後のデータフレームを返す関数の定義です。
やっていることは最初の方に書いた通り、データの塊に分類してより面積(データ個数)が大きな塊の位置情報からデータを抽出する関数です。
###################################################################### # csvを加工して切り出す関数 # 引数:読み込んだCSVのデータ # 戻り値:加工後のデータ ###################################################################### func_progcsv <- function(df){ # vectorのうちNAではない要素の数を返す func_vnum <- function(vec){sum(!is.na(vec))} # 行ごとに値の含まれている要素を返す max_collength <- df %>% apply(., 1, func_vnum) # データのブロックを作成する for(i in 1:length(max_collength)){ if(i == 1){ bulk_df <- data.frame(matrix(rep(NA, 3), nrow=1))[0,] pre_f <- 0 k <- 0 } f <- max_collength[i] if(f != pre_f){ k = k + 1 } bulk_df <- rbind(bulk_df, c(pre_f, f, k)) pre_f <- f } # データのブロックからデータ個数が最大となるブロックの情報を確認する bulkdf_summary <- bulk_df %>% as.tibble() %>% setNames(c("pre", "post", "no")) %>% mutate_at(vars(no), funs(as.factor)) %>% group_by(no) %>% summarise(colnum = max(post), datacount = sum(post)) %>% ungroup() %>% filter(datacount == max(datacount)) # 必要な列数と行のインデックスを取り出す ret_colnum <- bulkdf_summary$colnum ret_no <- bulkdf_summary$no ret_index <- which(bulk_df[,3] == ret_no) # 元のデータから切り出す prog_df <- df %>% slice(ret_index) %>% select(rep(1:ret_colnum)) %>% setNames(.[1,]) %>% slice(-1) return(prog_df) } ######################################################################
そしてここが今回のメインテーマであるNested Data Frameを使ったデータ処理です。
1:csvデータをすべて読み込みNested Data Frameを作成
2:Nested Data Frameに先ほど定義した加工用の関数を与えてCSVデータを処理する
3:ETF銘柄別にCSVファイルを出力するためにNested Data FrameをETF銘柄別にsplitする
ここで一点だけご注意いただきたいのは、read_csvは引数をデフォルト設定した場合、最初に読み込まれる5行目で列名が決定されるそうです。
今回のCSVファイルの場合も、デフォルトで読み込むと1~2列分しか読み込まれないことが殆どであるため、データの漏れがないように予め26列程余分なデータまで読み込んでいます。
df_fileinfo <- # ①:csvデータをすべて読み込みNested Data Frameを作成 data.frame(yymm = str_sub(csv_files, 1, 8), files = str_c(csv_path, csv_files), category = cate_etf) %>% as.tibble() %>% mutate_all(., funs(as.character)) %>% mutate(csvdf = map(.$files, ~read_csv(., col_names = letters[1:26]))) %>% # ②:Nested Data Frameにmap関数を使い先ほど定義した加工用の関数を与えてCSVデータを処理する mutate(csvdf = map(.$csvdf, ~func_progcsv(.))) %>% # ③:マージ用にNested Data FrameをETF銘柄別にsplitする select(-files) %>% split(.$category)
この時点で既に加工済みのデータフレームが得られたので最後にNested Data Frameをunnest()関数を使うことでETF銘柄別にデータをマージ。
最後にそれぞれCSV毎に出力して終了となります。
for(i in 1:length(df_fileinfo)){ print(i) output_df <- unnest(df_fileinfo[[i]]) output_filename <- names(df_fileinfo[i]) write.table(output_df, str_c("./csv/", output_filename, ".csv"), row.names = F, sep = ",") }
無事CSVファイルが出力され中身も整ったデータが得られました。
参考にさせていただいたサイトです。
この場で感謝申し上げます。ありがとうございました。
qiita.com
suryu.me
今回は以上になります。ここまで読んでくださりありがとうございます。
ようやく分析の段階に近づいてきましたね。
次回は集計、可視化、分析と考察をしてETF分析編は一旦完結させる予定です。
内容は簡単なものになると思います。
【ETF分析 ①データ収集編】Python Selenium+Beautifulsoup4を使ってETF構成銘柄を取得する
今回はPythonを使ってネットからETFの月次構成銘柄データを取得する方法です。
データ収集→前処理→分析の「データ収集」段階のみ触れます。
その後のフェーズは改めてブログにしたいと思います。
1スマートベータ
突然ですが、『スマートベータ』と呼ばれる少し前から話題の運用手法はご存知でしょうか。簡単に説明すると、時価総額や株価にウエイトを置くのではなく、配当、株価変動率など特定の要素にウエイトを置く投資手法です。
例えば配当利回りが高い銘柄のみに絞って投資する高配当戦略もスマートベータにあたりますね。研究も活発に行われておりこれからも益々発展が望める運用手法です。
2背景と目的
そんなスマートベータ戦略ですが自身の興味分野の一つでもあります。
特に最小分散やリスクパリティなどのリスクベースのポートフォリオ戦略に興味があります。
また株/金利/コモディティなどのマルチアセットによるアセットアロケーション手法にも興味があります。
それらの分析用データ集めの一環として、ETF商品の構成銘柄をざっと取得したいというのが今回の目的です。
一応マウス操作でもCSVファイルを落とせますが、ファイルが月単位なので取得月数分ポチポチするのは非常に効率が悪いので、PythonのSeleniumとBeautifulsoupを使って取得したいと思います。
今回はBlackRock社の「iシェアーズETF 東証上場シリーズ」から取得することにします。
(SeleniumやBeautifulsoupそのものの説明は今回は省略します)
3Pythonコード
必要なモジュールのインポート
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import Select from bs4 import BeautifulSoup import time import requests import urllib.request
seleniumでブラウザを起動する
driver = webdriver.Chrome(executable_path = "(SeleniumChromeドライバのパス)/chromedriver.exe")
BlackRock isharesの東証上場ETF一覧ページにアクセスして商品一覧と対応する商品ページのURLを辞書型にする
# BlackRock isharesの東証上場ETF一覧ページにアクセス url = 'https://www.blackrock.com/jp/individual/ja/strategies/ishares-tse/' driver.get(url) time.sleep(1) # BeautifulSoupでパース soup = BeautifulSoup(driver.page_source, "html.parser") # 予めETF一覧テーブルの情報を取得する etf_table = soup.find("table", attrs={"class":"ishares-tse-table"}) # a要素を取得してurlとリンクの辞書を作成する etf_link = [a.get("href") for a in etf_table.find_all("a")] etf_name = [a.text.replace(" ", "").replace("・","") for a in etf_table.find_all("a")] etf_dict = dict(zip(etf_name, etf_link))
ダウンロード先を指定しておく
dir_post = 'C:/Users/(ダウンロード先)/'
ダウンロード用の関数を定義
def download_etfcsv(etf_name, etf_url): # urlにアクセスする driver.get(etf_url) time.sleep(1) #すべての銘柄を見るボタンをクリック driver.find_element_by_xpath('//*[@id="holdingsTabs"]/ul/li[2]/a').click() time.sleep(1) # 基準日を選択する操作がドロップダウン形式となっているため、連続的に操作を行う cnt = 0 # ドロップダウンを一つずつ選択してDLする for cnt in range(10000): try: # エレメントの取得 dropdown_elm = driver.find_elements_by_class_name("date-dropdown") # 取得したいエレメントをSelectタグに対応したエレメントに変化 dropdown_elm_select = Select(dropdown_elm[1]) # ドロップダウンから日付を変更する dropdown_elm_select.select_by_index(cnt) # 選択した基準日を取得する time.sleep(3) base_date = dropdown_elm[1].get_attribute("value") # urlを取得する url = driver.find_elements_by_class_name("icon-xls-export") url = url[2].get_attribute("href") # csvファイルを保存する print('download date:{} name:{}'.format(base_date, etf_name)) urllib.request.urlretrieve(url, dir_post + str(base_date) + "_" + etf_name + ".csv") time.sleep(3) except: break
ダウンロードを実行
(実行には1時間程度要するのでご注意ください)
for etf in etf_dict: print(etf) download_etfcsv(etf, etf_dict[etf])
ブラウザを閉じる
driver.quit()
Pythonコードは以上になります。