Yahooファイナンスの株価を取得してみる

RでYahooファイナンスを取得するモジュール、RFinanceYJを改造し使っていたのですが、あまりの遅さに閉口してしまいました。丸一日掛けても14年分の日次株価は1000銘柄程度しか撮れていません。やり方はこの辺りにまとめています。

http://d.hatena.ne.jp/anagotan/searchdiary?word=%2A%5BR%5D

RaspberryPiを手に入れたこともあり、pythonで書き直してみました。pythonは2.7です

master.py

マスターを取得します 

#!/bin/env python
# coding:utf-8

import urllib
import urllib2
from lxml import etree
import time


def get_stockmaster(hira):
	p=1
	ret="dummy"
	ss=""
	while(ret!=""):
		ret,plen=get_stockmaster_page(hira,p)
		#print ret
		if ret != "":
			ss=ss+ret
		p=p+1
	return ss

def get_stockmaster_page(hira,p):
	hira=hira.encode('utf-8')
	url="http://stocks.finance.yahoo.co.jp/stocks/qi/?%s"
	params={"js":hira,"p":p}
	data=urllib.urlencode(params)
	#print data
	res=urllib.urlopen(url % data)
	page=res.read()
	root=etree.fromstring(page,etree.HTMLParser())
	elem=root.xpath("//a[contains(@href,'/stocks/detail')]")
	i=0
	ret=""
	for ele in elem:
		if ele.text != None:
			if i%3==0:
				ret=ret+ele.text+"\t"
			if i%3==1:
				ret=ret+ele.text.encode('utf-8')+"\n"
		i=i+1
	time.sleep(1)
	return ret,len(elem)

def get_stockmasterall():
	hira=u"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわ"
	size=len(hira)
	ss=""
	for i in range(0,size):
		print hira[i:i+1]
		#print i
		ss=ss+get_stockmaster(hira[i:i+1])
	f=open("data/master.txt","w")
	f.write(ss)
	f.close()

if __name__ == '__main__':
	get_stockmasterall()

hist.py

時系列データを取得します 

#!/bin/env python
# coding:utf-8

import urllib
import urllib2
from lxml import etree
import time


def get_stockdata(code,sy,sm,sd,ey,em,ed,tm):
	p=1
	plen=53
	ss=""
	while(plen>=53):
		ret,plen=get_stockdata_page(code,sy,sm,sd,ey,em,ed,tm,p)
		ss=ss+ret
		p=p+1
	f=open("data/"+code+".txt","w")
	f.write(ss)
	f.close()

def get_stockdata_page(code,sy,sm,sd,ey,em,ed,tm,p):
	url="http://info.finance.yahoo.co.jp/history/?%s"
	params={"code":code,"sy":sy,"sm":sm,"sd":sd,"ey":ey,"em":em,"ed":ed,"tm":tm,"p":p}
	data=urllib.urlencode(params)
	#print data
	res=urllib.urlopen(url % data)
	page=res.read()
	root=etree.fromstring(page,etree.HTMLParser())
	elem=root.xpath("//table")
	
	ret=""
	plen=len(elem[1].xpath("//tr"))
	for tr in elem[1].xpath("//tr"):
		if len(tr.findall("td"))==7:
			str=""
			for td in tr.findall("td"):
				s=td.text.encode('utf-8').replace(',','').replace('年','-').replace('月','-').replace('日','')
				str=str+s+"\t"
			str=str+code
			ret=ret+str+"\n"
	time.sleep(1)
	return ret,plen

def get_stockdata_all(sy,sm,sd,ey,em,ed,tm):
	f=open("data/master.txt")
	lines=f.readlines()
	f.close()
	for line in lines:
		ay=line.split("\t")
		print ay[0]
		get_stockdata(ay[0],sy,sm,sd,ey,em,ed,tm)

if __name__ == '__main__':
	get_stockdata_all("2000","01","01","2014","08","27","d")

やっつけで作成しましたがとりあえずファイルに落とすところまでは確認できました。

ちなみに動作環境はRapberryPITypeB+のRASPBIANVersion:June 2014で動いています

Raspberry Pi TypeB+で遊ぶ

昨日ポチッたRaspberryPi、早速届きました。RSコンポーネンツさん仕事が早いです。

早速セットアップ。

MicroSD

上海問屋で以前入手したトラセンドの32G

microUSB電源アダプタ

ソニーエリクソンのMW1付属のものを流用。

モニタ

ソニーの液晶テレビ

要は家にある物でなんとかしてみました。セットアップはMacOSXで行います

SDのフォーマット

通常のフォーマットでFAT32にしておけばOK。

raspberry piのImageのダウンロード

http://www.raspberrypi.org/downloads/

こちらからRASPBIANのZIPファイルをダウンロード。解凍しておきます

イメージの書き込み

ディスクをマウントしコマンドプロンプトのdfで確認します

$ df
Filesystem		512-blocks			 Used	Available Capacity	 iused		 ifree %iused	Mounted on
/dev/disk0s2	1951845952	408391536 1542942416		21%	51112940 192867802	 21%	 /
devfs								380				380					0	 100%			 659				 0	100%	 /dev
map -hosts						 0					0					0	 100%				 0				 0	100%	 /net
map auto_home					0					0					0	 100%				 0				 0	100%	 /home
/dev/disk1s2	1952851232 1666536368	286314864		86% 208317044	35789358	 85%	 /Volumes/WININSTALL
/dev/disk2s1		61800928			 3648	 61797280		 1%				 0				 0	100%	 /Volumes/UNTITLED

/dev/disk2s1がSDのデバイスです。マウントを外し書き込んでいきます。

$ sudo diskutil umountDisk /dev/disk2	 # /dev/disk2s1じゃないよ
$ sudo dd bs=1m if=2014-06-20-wheezy-raspbian.img of=/dev/disk2
2825+0 records in
2825+0 records out
2962227200 bytes transferred in 382.700174 secs (7740334 bytes/sec)

4、5分でイメージが書き終わります。

起動

HDMIケーブルをテレビと接続し、SDカードをセットしたら電源コネクタにマイクロUSBの電源を差し込みます。テレビに起動コンソールが映り始めればOK

だめな場合には、LEDランプの緑がちかちかしているかどうか確認します。こちらがどうやらディスクアクセスのランプのようです。うまく起動画面が出ない場合にはSDにOSがうまく書き込めていないと思いますので、ディスクを変更するなどし再度チャレンジしてください。

RaspberryPIのセットアップ

起動したらディスクやLocaleの設定などを行います

$ sudo raspi-config

1.Expand Filesystem

そのままの設定だと2GしかSDカードを使ってくれないのでこちらのメニューからすべての領域を使うようにします

4.Internationalisation Options

I1 Change Locale

ロケールをja_JP.UTF-8へと変更

I2 Change Timezone

Tokyoへ変更

I3 Change Keyboard Layout

General Keyboard 105 あたりからJapaneseを選択

8. Advanced Options

A4 SSH

これをONにしておくと外部からSSHでログイン可能

ここまででだいぶ使いやすくなるかと思います。あと固定IPの方が使いやすいのでネットワークの設定を行います。

$ sudo vi /etc/network/interfaces
#iface eth0 inet dhcp					 # ここをコメントアウトし
iface eth0 inet static
address 192.168.11.101		 # この辺りの設定をする 
netmask 255.255.255.0
gateway 192.168.11.254

自宅のネットワーク構成に合わせてaddressとgatewayを設定します。

これで一通りの設定ができたかと思います。必要なパッケージはapt-getが使えるので簡単にインストールできるのが楽ですね

Rでチャートを書いてみる(8)

前回までのスクリプトをぐるぐる回すようにしてみました。

一銘柄ずつとると時間がかかるので並列で動くようにしています

  • RFinanceYJPatch.R


quoteStockMasterTsData <- function(){

	financial.data <- data.frame(NULL)

	function.stockMasterData<-function(hira){
		r <- NULL
		result.num <- 20
		master.data <- data.frame(NULL)
		start.num<-0
		while( result.num >= 20 ){
			start.num <- start.num + 1
			quote.table <- NULL
			quote.url <- paste('http://stocks.finance.yahoo.co.jp/stocks/qi/?js=',hira,'&p=',start.num,sep="")
		
			try( r <- xmlRoot(htmlTreeParse(quote.url,error=xmlErrorCumulator(immediate=F))), TRUE)
			if( is.null(r) ) stop(paste("Can not access :", quote.url))

			try( quote.table <- xpathApply(r,"//a[contains(@href,'/stocks/detail')]"), TRUE )
			
			if( is.null(quote.table) ){
				if( is.null(master.data) ){
					stop(paste("Can not quote :", x))
				}else{
					return(master.data)
				}
			}

			size <- xmlSize(quote.table)/3
			if(size==0){
					return(master.data)
			}
			for(i in 1:size){
				mtmp<-data.frame(code=xmlValue(quote.table[[i*3-2]]),name=xmlValue(quote.table[[i*3-1]]))
				mtmp$code<-as.character(mtmp$code)
				mtmp$name<-as.character(mtmp$name)
				master.data <- rbind(master.data,mtmp)
			}
			
			result.num <- xmlSize(quote.table)/3
			Sys.sleep(1)
		}
		return(master.data)
	}

	hiraList<-"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわ"
	for(i in 1:nchar(hiraList)){
		hira<-substring(hiraList,i,i)
		master.data<-function.stockMasterData(hira)
		financial.data<-rbind(financial.data,master.data)

	}
	financial.data <- financial.data[order(financial.data$code),]
	return(financial.data)	

}


#API
quoteStockTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.stock <- function(quote.table.item){
		if( xmlSize(quote.table.item) < 5) return(NULL) 
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		o <- as.number(xmlValue(quote.table.item[[2]]))
		h <- as.number(xmlValue(quote.table.item[[3]]))
		l <- as.number(xmlValue(quote.table.item[[4]]))
		c <- as.number(xmlValue(quote.table.item[[5]]))
		v <- ifelse(xmlSize(quote.table.item) >= 6,as.number(xmlValue(quote.table.item[[6]])),0)
		a <- ifelse(xmlSize(quote.table.item) >= 7,as.number(xmlValue(quote.table.item[[7]])),0)
		return(data.frame(date=d,open=o,high=h,low=l,close=c,volume=v, adj_close=a))
	}
	return(quoteTsData(x,function.stock,since,start.num,date.end,time.interval,type="stock"))
}
quoteFundTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.fund <- function(quote.table.item){
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		if(time.interval=='monthly'){
			d <- endOfMonth(d)
		}
		c <- as.number(xmlValue(quote.table.item[[2]]))
		v <- as.number(xmlValue(quote.table.item[[3]]))
		return(data.frame(date=d,constant.value=c,NAV=v))
	}
	return(quoteTsData(x,function.fund,since,start.num,date.end,time.interval,type="fund"))
}
quoteFXTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.fx <- function(quote.table.item){
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		o <- as.number(xmlValue(quote.table.item[[2]]))
		h <- as.number(xmlValue(quote.table.item[[3]]))
		l <- as.number(xmlValue(quote.table.item[[4]]))
		c <- as.number(xmlValue(quote.table.item[[5]]))
		return(data.frame(date=d,open=o,high=h,low=l,close=c))
	}
	return(quoteTsData(x,function.fx,since,start.num,date.end,time.interval,type="fx"))
}
######	private functions	#####
#get time series data from Yahoo! Finance.
quoteTsData <- function(x,function.financialproduct,since,start.num,date.end,time.interval,type="stock"){
	r <- NULL
	result.num <- 51
	financial.data <- data.frame(NULL)
	#start <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&c=\\1&a=\\2&b=\\3",since))
	#end	 <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&f=\\1&d=\\2&e=\\3",date.end))
	start <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&sy=\\1&sm=\\2&sd=\\3",since))
	end	 <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&ey=\\1&em=\\2&ed=\\3",date.end))

	if(!any(time.interval==c('d','w','m'))) stop("Invalid time.interval value")
	
	extractQuoteTable <- function(r,type){
		if(type %in% c("fund","fx")){
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[9]][[2]]
		}
		else{
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[10]][[2]]
		}
		return(tbl)
	}
	
	while( result.num >= 51 ){
		start.num <- start.num + 1
		quote.table <- NULL
		quote.url <- paste('http://info.finance.yahoo.co.jp/history/?code=',x,start,end,'&p=',start.num,'&tm=',substr(time.interval,1,1),sep="")
	
		try( r <- xmlRoot(htmlTreeParse(quote.url,error=xmlErrorCumulator(immediate=F))), TRUE)
		if( is.null(r) ) stop(paste("Can not access :", quote.url))

		#try( quote.table <- r[[2]][[1]][[1]][[16]][[1]][[1]][[1]][[4]][[1]][[1]][[1]], TRUE )
		#try( quote.table <- extractQuoteTable(r,type), TRUE )
		try( quote.table <- xpathApply(r,"//table")[[2]], TRUE )
		
		if( is.null(quote.table) ){
			if( is.null(financial.data) ){
				stop(paste("Can not quote :", x))
			}else{
				 financial.data <- financial.data[order(financial.data$date),]
				 return(financial.data)
			}
		}

		size <- xmlSize(quote.table)
		for(i in 2:size){
			financial.data <- rbind(financial.data,function.financialproduct(quote.table[[i]]))
		}
		
		result.num <- xmlSize(quote.table)
		Sys.sleep(1)
	}
	financial.data <- financial.data[order(financial.data$date),]
	return(financial.data)	
}
#convert string formart date to POSIXct object
convertToDate <- function(date.string,time.interval)
{
	#data format is different between monthly and dialy or weekly
	if(any(time.interval==c('d','w'))){
		result <- gsub("^([0-9]{4})([^0-9]+)([0-9]{1,2})([^0-9]+)([0-9]{1,2})([^0-9]+)","\\1-\\3-\\5",date.string)
	}else if(time.interval=='m'){
		result <- gsub("^([0-9]{4})([^0-9]+)([0-9]{1,2})([^0-9]+)","\\1-\\3-01",date.string)
	}
	return(as.POSIXct(result))
}
#convert string to number.
as.number <- function(string)
{
	return(as.double(as.character(gsub("[^0-9.]", "",string))))
}
#return end of month date.
endOfMonth <- function(date.obj)
{
	startOfMonth		 <- as.Date(format(date.obj,"%Y%m01"),"%Y%m%d")
	startOfNextMonth <- as.Date(format(startOfMonth+31,"%Y%m01"),"%Y%m%d")
	return(startOfNextMonth-1)
}


library(RSQLite)

# fromDate: yyyy-MM-dd
getStockHistoricalData<-function(sqliteFile,codes,fromDate){
	drv<-dbDriver("SQLite")
	con<-dbConnect(drv,dbname=sqliteFile)
	try(rs<-dbSendQuery(con,"drop table historicalData"))
	rs<-dbSendQuery(con,"create table historicalData (date text,open real,high real,
		low real,close real,volume real,adj_close real,code text)")
	rs<-dbSendQuery(con,"create unique index idx_historicalData on historicalData(code,date)")

	#stockMaster<-quoteStockMasterTsData()

	for(code in codes){
		print(paste("code=",code,sep=""))
		tryCatch({
			data<-quoteStockTsData(code,fromDate)
			data<-transform(data,code=code)
			data$code<-as.character(data$code)
			data$date<-as.character(data$date)
			dbBeginTransaction(con)
			dbSendQuery(con,paste("delete from historicalData where code='",code,"'",sep=""))
			dbSendPreparedQuery(con,"insert into historicalData
				(date,open,high,low,close,volume,adj_close,code)
				values(:date, :open, :high, :low, :close, :volume, :adj_close, :code)",bind.data=data)
			dbCommit(con)
		},
		error = function(e){
			message(paste("ERROR:",code))
			message(e)
		
		})
	}
}

  • getall.sh
#!/bin/sh

getStockData(){
fromNo=$1
toNo=$2
fileNo=$3

/usr/bin/R --vanilla << Eof
library(RFinanceYJ)
source("/Users/utsuboka/Documents/prog/RFinanceYJPatch.R")
stockMaster<-quoteStockMasterTsData()

getStockHistoricalData("stock$fileNo.db",stockMaster\$code[$fromNo:$toNo],"2000-01-01") 
Eof
}

getStockData 1 500 1 &
getStockData 501 1000 2 & 
getStockData 1001 1500 3 &
getStockData 1501 2000 4 & 
getStockData 2001 2500 5 &
getStockData 2501 3000 6 & 
getStockData 3001 3500 7 &
getStockData 3501 4000 8 & 

現在4000銘柄弱ですのでこれでOKだと思います。ただこれでもまだまだ時間がかかりますが全部取得できるかと思います

Rでチャートを書いてみる(7)

前回でマスターまで取得ができたので、このマスターを用い、全銘柄分のデータを取得することにします。

取得したデータはリレーショナルデータベースへ格納するのが一般的かと思いますので、ここは簡単に扱えるSQLiteに入れることにします。

SQLiteをインストール

MacOSXの場合には以下のコマンドで一発です

brew install sqlite

つぎにRSQLiteパッケージをインストールします

install.packages("RSQLite")

準備が整ったらスクリプトを用意しこれを実行します

(前回までのRFinaiceYJの拡張関数を実行しておくこと)

library(RSQLite)
drv<-dbDriver("SQLite")
con<-dbConnect(drv,dbname="stock.db")
rs<-dbSendQuery(con,"drop table historicalData")
rs<-dbSendQuery(con,"create table historicalData (date text,open real,high real,
	low real,close real,volume real,adj_close real,code text)")
rs<-dbSendQuery(con,"create unique index idx_historicalData on historicalData(code,date)")

stockMaster<-quoteStockMasterTsData()

for(code in stockMaster$code){
	#cat(paste("code=",code,"\n",sep=""))
	data<-quoteStockTsData(code,"2010-01-01")
	data<-transform(data,code=code)
	data$code<-as.character(data$code)
	data$date<-as.character(data$date)
	dbBeginTransaction(con)
	dbSendQuery(con,paste("delete from historicalData where code='",code,"'",sep=""))
	dbSendPreparedQuery(con,"insert into historicalData
		(date,open,high,low,close,volume,adj_close,code)
	 	values(:date, :open, :high, :low, :close, :volume, :adj_close, :code)",bind.data=data)
	dbCommit(con)
}

かなり時間がかかると思いますが無事SQLiteにはいっていることが確認できます

sqlite> select * from historicalData;
2010-01-04|293.0|294.0|290.0|291.0|28100.0|291.0|3076
2010-01-05|293.0|293.0|288.0|288.0|81500.0|288.0|3076
..
2014-08-19|1534.0|1536.0|1505.0|1505.0|22900.0|1505.0|3791
2014-08-20|1505.0|1520.0|1505.0|1516.0|7100.0|1516.0|3791
  • 2014-8-24

もう少し使い勝手よくしてみました

library(RSQLite)

# fromDate: yyyy-MM-dd
getStockHistricalData<-function(stockMaster,fromDate){
	drv<-dbDriver("SQLite")
	con<-dbConnect(drv,dbname="stock.db")
	rs<-dbSendQuery(con,"drop table historicalData")
	rs<-dbSendQuery(con,"create table historicalData (date text,open real,high real,
		low real,close real,volume real,adj_close real,code text)")
	rs<-dbSendQuery(con,"create unique index idx_historicalData on historicalData(code,date)")

	#stockMaster<-quoteStockMasterTsData()

	for(code in stockMaster$code){
		print(paste("code=",code,sep=""))
		tryCatch({
			data<-quoteStockTsData(code,fromDate)
			data<-transform(data,code=code)
			data$code<-as.character(data$code)
			data$date<-as.character(data$date)
			dbBeginTransaction(con)
			dbSendQuery(con,paste("delete from historicalData where code='",code,"'",sep=""))
			dbSendPreparedQuery(con,"insert into historicalData
				(date,open,high,low,close,volume,adj_close,code)
			 	values(:date, :open, :high, :low, :close, :volume, :adj_close, :code)",bind.data=data)
			dbCommit(con)
		},
		error = function(e){
			message(paste("ERROR:",code))
			message(e)
		
		})
	}
}

Rでチャートを書いてみる(6)

株価を利用する際には株価コードの一覧が必要となってきます。そこで調子に乗って株価コードの一覧を取得してみました

例によってRFinanceYJに追加する関数です

quoteStockMasterTsData <- function(){

	financial.data <- data.frame(NULL)

	function.stockMasterData<-function(hira){
		r <- NULL
		result.num <- 20
		master.data <- data.frame(NULL)
		start.num<-0
		while( result.num >= 20 ){
			start.num <- start.num + 1
			quote.table <- NULL
			quote.url <- paste('http://stocks.finance.yahoo.co.jp/stocks/qi/?js=',hira,'&p=',start.num,sep="")
		
			try( r <- xmlRoot(htmlTreeParse(quote.url,error=xmlErrorCumulator(immediate=F))), TRUE)
			if( is.null(r) ) stop(paste("Can not access :", quote.url))

			try( quote.table <- xpathApply(r,"//a[contains(@href,'/stocks/detail')]"), TRUE )
			
			if( is.null(quote.table) ){
				if( is.null(master.data) ){
					stop(paste("Can not quote :", x))
				}else{
					return(master.data)
				}
			}

			size <- xmlSize(quote.table)/3
			if(size==0){
					return(master.data)
			}
			for(i in 1:size){
				mtmp<-data.frame(code=xmlValue(quote.table[[i*3-2]]),name=xmlValue(quote.table[[i*3-1]]))
				mtmp$code<-as.character(mtmp$code)
				mtmp$name<-as.character(mtmp$name)
				master.data <- rbind(master.data,mtmp)
			}
			
			result.num <- xmlSize(quote.table)/3
			Sys.sleep(1)
		}
		return(master.data)
	}

	hiraList<-"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわ"
	for(i in 1:nchar(hiraList)){
		hira<-substring(hiraList,i,i)
		master.data<-function.stockMasterData(hira)
		financial.data<-rbind(financial.data,master.data)

	}
	financial.data <- financial.data[order(financial.data$code),]
	return(financial.data)	

}

YahooFinanceの銘柄名の頭文字から一覧を得るページがあるのでこちらからダウンロードするようにしました。

結果ですが、ページ数が多いので若干時間がかかります。

> stockMaster<-quoteStockMasterTsData()
> stockMaster
		 code																						 name
1		3076											 あい ホールディングス(株)
2		7013																			 (株)IHI
3		4812																				 ISID
4		9753											 アイエックス・ナレッジ(株)
..
3451 9927																	 (株)ワットマン
3452 2918																 わらべや日洋(株)
3453 3344										 (株)ワンダーコーポレーション
3454 7564																	 (株)ワークマン
3455 2429										 (株)ワールドホールディングス

全銘柄とれるようになりました!

  • 2014.8.17

銘柄コード純になるように修正してみました

Rでチャートを書いてみる(5)

いつの間にかQiitaに引用されていました。(^^!

http://qiita.com/HirofumiYashima/items/4b3a3a3e49be59d81e4f

うまく動かないとのことなので修正をしてみました。

RFinanceYJはYahooのHTML変更に振り回されているようで、うまく動かない場合が多いです。

> library(RFinanceYJ)
> quoteStockTsData("6758.t")
 以下にエラー order(financial.data$date) :	引数 1 がベクトルではありません 

どうもStockTsDataの↓の部分でテーブルの形を決まりきった物として扱っているので、Yahoo側のちょっとした変更(たとえば上の方になんか広告などを追加するとか)に負けてしまっています。

	extractQuoteTable <- function(r,type){
		if(type %in% c("fund","fx")){
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[9]][[2]]
		}
		else{
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[10]][[2]]
		}
		return(tbl)
	}
	
	while( result.num >= 51 ){
		start.num <- start.num + 1
		quote.table <- NULL
		quote.url <- paste('http://info.finance.yahoo.co.jp/history/?code=',x,start,end,'&p=',start.num,'&tm=',substr(time.interval,1,1),sep="")

..

		#try( quote.table <- r[[2]][[1]][[1]][[16]][[1]][[1]][[1]][[4]][[1]][[1]][[1]], TRUE )
		try( quote.table <- extractQuoteTable(r,type), TRUE )

Xpathで解析するようにし、若干の変更には耐えられるように修正してみました。この株価ページにはテーブルが3個あり、その2番目が株価のテーブルですので、最新版の0.3.1にまたまたパッチを当ててみました。こう書き換えます。

		#try( quote.table <- extractQuoteTable(r,type), TRUE )
		try( quote.table <- xpathApply(r,"//table")[[2]], TRUE )

この関数だけを読み込んでもだめなので全体を記述しそれをlibrary(RFinanceYJ)に続けて読んでやります

library(RFinanceYJ)
#API
quoteStockTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.stock <- function(quote.table.item){
		if( xmlSize(quote.table.item) < 5) return(NULL) 
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		o <- as.number(xmlValue(quote.table.item[[2]]))
		h <- as.number(xmlValue(quote.table.item[[3]]))
		l <- as.number(xmlValue(quote.table.item[[4]]))
		c <- as.number(xmlValue(quote.table.item[[5]]))
		v <- ifelse(xmlSize(quote.table.item) >= 6,as.number(xmlValue(quote.table.item[[6]])),0)
		a <- ifelse(xmlSize(quote.table.item) >= 7,as.number(xmlValue(quote.table.item[[7]])),0)
		return(data.frame(date=d,open=o,high=h,low=l,close=c,volume=v, adj_close=a))
	}
	return(quoteTsData(x,function.stock,since,start.num,date.end,time.interval,type="stock"))
}
quoteFundTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.fund <- function(quote.table.item){
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		if(time.interval=='monthly'){
			d <- endOfMonth(d)
		}
		c <- as.number(xmlValue(quote.table.item[[2]]))
		v <- as.number(xmlValue(quote.table.item[[3]]))
		return(data.frame(date=d,constant.value=c,NAV=v))
	}
	return(quoteTsData(x,function.fund,since,start.num,date.end,time.interval,type="fund"))
}
quoteFXTsData <- function(x, since=NULL,start.num=0,date.end=NULL,time.interval='daily')
{
	time.interval <- substr(time.interval,1,1)
	function.fx <- function(quote.table.item){
		d <- convertToDate(xmlValue(quote.table.item[[1]]),time.interval)
		o <- as.number(xmlValue(quote.table.item[[2]]))
		h <- as.number(xmlValue(quote.table.item[[3]]))
		l <- as.number(xmlValue(quote.table.item[[4]]))
		c <- as.number(xmlValue(quote.table.item[[5]]))
		return(data.frame(date=d,open=o,high=h,low=l,close=c))
	}
	return(quoteTsData(x,function.fx,since,start.num,date.end,time.interval,type="fx"))
}
######	private functions	#####
#get time series data from Yahoo! Finance.
quoteTsData <- function(x,function.financialproduct,since,start.num,date.end,time.interval,type="stock"){
	r <- NULL
	result.num <- 51
	financial.data <- data.frame(NULL)
	#start <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&c=\\1&a=\\2&b=\\3",since))
	#end	 <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&f=\\1&d=\\2&e=\\3",date.end))
	start <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&sy=\\1&sm=\\2&sd=\\3",since))
	end	 <- (gsub("([0-9]{4,4})-([0-9]{2,2})-([0-9]{2,2})","&ey=\\1&em=\\2&ed=\\3",date.end))

	if(!any(time.interval==c('d','w','m'))) stop("Invalid time.interval value")
	
	extractQuoteTable <- function(r,type){
		if(type %in% c("fund","fx")){
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[9]][[2]]
		}
		else{
			tbl <- r[[2]][[2]][[7]][[3]][[3]][[10]][[2]]
		}
		return(tbl)
	}
	
	while( result.num >= 51 ){
		start.num <- start.num + 1
		quote.table <- NULL
		quote.url <- paste('http://info.finance.yahoo.co.jp/history/?code=',x,start,end,'&p=',start.num,'&tm=',substr(time.interval,1,1),sep="")
	
		#try( r <- xmlRoot(htmlTreeParse(quote.url,error=xmlErrorCumulator(immediate=F))), TRUE)	# これだと取得時にエラーが出た。。
		try(r<-htmlParse(quote.url))
		if( is.null(r) ) stop(paste("Can not access :", quote.url))

		#try( quote.table <- r[[2]][[1]][[1]][[16]][[1]][[1]][[1]][[4]][[1]][[1]][[1]], TRUE )
		#try( quote.table <- extractQuoteTable(r,type), TRUE )
		try( quote.table <- xpathApply(r,"//table")[[2]], TRUE )
		
		if( is.null(quote.table) ){
			if( is.null(financial.data) ){
				stop(paste("Can not quote :", x))
			}else{
				 financial.data <- financial.data[order(financial.data$date),]
				 return(financial.data)
			}
		}

		size <- xmlSize(quote.table)
		for(i in 2:size){
			financial.data <- rbind(financial.data,function.financialproduct(quote.table[[i]]))
		}
		
		result.num <- xmlSize(quote.table)
		Sys.sleep(1)
	}
	financial.data <- financial.data[order(financial.data$date),]
	return(financial.data)	
}
#convert string formart date to POSIXct object
convertToDate <- function(date.string,time.interval)
{
	#data format is different between monthly and dialy or weekly
	if(any(time.interval==c('d','w'))){
		result <- gsub("^([0-9]{4})([^0-9]+)([0-9]{1,2})([^0-9]+)([0-9]{1,2})([^0-9]+)","\\1-\\3-\\5",date.string)
	}else if(time.interval=='m'){
		result <- gsub("^([0-9]{4})([^0-9]+)([0-9]{1,2})([^0-9]+)","\\1-\\3-01",date.string)
	}
	return(as.POSIXct(result))
}
#convert string to number.
as.number <- function(string)
{
	return(as.double(as.character(gsub("[^0-9.]", "",string))))
}
#return end of month date.
endOfMonth <- function(date.obj)
{
	startOfMonth		 <- as.Date(format(date.obj,"%Y%m01"),"%Y%m%d")
	startOfNextMonth <- as.Date(format(startOfMonth+31,"%Y%m01"),"%Y%m%d")
	return(startOfNextMonth-1)
}

実行結果

> quoteStockTsData("6758.t",since="2010-01-01")
					 date	 open	 high		low	close		volume adj_close
1136 2010-01-04 2700.0 2744.0 2694.0 2731.0	 4004500		2731.0
1135 2010-01-05 2781.0 2782.0 2704.0 2719.0	 4894300		2719.0
1134 2010-01-06 2719.0 2757.0 2699.0 2744.0	 4270900		2744.0
1133 2010-01-07 2750.0 2764.0 2723.0 2743.0	 2899600		2743.0
1132 2010-01-08 2785.0 2809.0 2769.0 2809.0	 6981100		2809.0
..

長いヒストリカルデータも何のその!!

2014.8.19

HirofumiYashima様の指摘により修正してみました

なお、実行環境はMacOS10.9のR version 3.0.3 です

2015.1.20

こちらの記事はYahooの仕様変更によりとれなくなってしまいました

http://d.hatena.ne.jp/anagotan/20150120

こちらを参照ください

Backbone.UndoをTypescriptで使ってみた その3

昨日のサンプルは、クリックイベントの処理がモデルの中に入っているので、ちょっとイマイチだったので、コントローラ側でその辺りの処理をするように再度修正してみました。また、ArrayなどのUndoに対応できる用にコントローラ側でデータ修正する際に、深いコピーでモデルに入れるようにしないといけないのでその辺りも合わせて修正しました。

  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");
		var itemButton = $("#item_button");
		var attrButton = $("#attr_button");

		export class App {
				private listview: ItemListView;
				private appModel: AppModel;
				constructor() {
						var self = this;
						self.appModel = new AppModel();

						self.listview = new ItemListView({
								"appModel": self.appModel			// collectionのエイリアスを持つのではダメ、ポインタが書き換わるため
						});

						undoButton.click(function () {
								console.log("undoButton.click");
								self.appModel.undoHandler.undo();
								self.draw();
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								self.appModel.undoHandler.redo();
								self.draw();
						});

						addButton.click(function () {
								console.log("addButton.click");
								var ary = >self.appModel.get("collection");
								var newary = new Array();// 新しくオブジェクトを作らないとstack.onしない
								ary.forEach(function (item) {
										newary.push(item);
								});
								newary.push({ "name": "0", "value": "0" });
								self.appModel.set({ "collection": newary });
								//self.listview.list = self.appModel.get("collection");
								self.draw();
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								var newary = new Array();
								self.appModel.get("collection").forEach(function (item) {
										newary.push({"value":item.value,"name":_.random(100).toString() }); // 新しくオブジェクトを作らないとstack.onしない
								});
								self.appModel.set({ "collection": newary });
								self.draw();
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								var newary = new Array();
								self.appModel.get("collection").forEach(function (item) {
										newary.push({ "name": item.name, "value": _.random(100).toString() });
								});
								self.appModel.set({ "collection": newary });
								self.draw();
						});

						itemButton.click(function () {
								console.log("itemButton.click");
								self.appModel.set({
										"item_item": { "item_name": _.random(100).toString(), "item_value": _.random(100).toString() }
								});
								self.draw();
						});
						attrButton.click(function () {
								console.log("attrButton.click");
								self.appModel.set({ "attr": _.random(100).toString() });
								self.draw();
						});

				}

				draw(): void {
						console.log("draw");
						this.listview.render();
						$("#item_name").text(this.appModel.get("item_item").item_name);
						$("#item_value").text(this.appModel.get("item_item").item_value);
						$("#attr").text(this.appModel.get("attr"));
				}
		}

		export class AppModel extends Backbone.Model {
				undoHandler: UndoHandler;

				constructor(options?) {
						super(options);
						this.undoHandler = new UndoHandler(this);
						this.bind("change", this.change);
						
				}
				defaults() {
						return {
								collection:[
										{ "name": "0", "value": "0" }
								],
								item_item: {
										"item_name": "",
										"item_value":""
								},
								attr:"",
						}
				}

				change(): void {

						console.log("AppModel.change");
				}

		}

		export class UndoHandler {
				private manager: Backbone.UndoManager;
				 constructor(collections:any) {


						var self = this;
						 self.manager = new Backbone.UndoManager({ register: collections, track: true });

						 self.manager.removeUndoType("change:isChanging");
						 self.manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});


						 self.manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						 self.manager.trigger("undo redo");

						 self.manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

						

				 }
				undo(): void{
						this.manager.undo(true);
				}
				redo(): void {
						this.manager.redo(true);
				}
		}


		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
					return {
								"id": _.uniqueId(),
								"name": "",
								"value":""
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange"
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
				appModel: any; // backbone.model
				constructor(options?) {
						var self = this;
						this.el = "#item-list";
						if (options) {
								if (options.appModel) this.appModel = options.appModel;
						}
						
						this.collection = new Backbone.Collection();
						this.appModel.get("collection").forEach(function (item) {
								self.collection.add(new ItemModel(item));
						});

						super(options);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						var self = this;

						self.collection = new Backbone.Collection();
						this.appModel.get("collection").forEach(function (item) {
								self.collection.add(new ItemModel(item));
						});
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();

  • index.html




		
		






		

item_name:
,item_value:

attr:




次に単体テストをmochaを使用して記述してみます。

予めmocha.js,mocha.d.ts,mocha.css,asert.js,assert.d.tsを用意しておく必要があります。

書いていてわかったのですが、実はBackbone.undo.jsはデータをスタックする際にタイムスタンプを用いているらしいので、テストユニットでババっと流すと、同じタイムスタンプでスタックされてしまい、思い通りの動きになりません。

そこで、テストユニットにはsettimeoutで少し実行間隔を開けてやることにします。

  • app-test.ts
///
///


module sample {

		describe("テスト", () => {

				it("UndoRedoTest", (done) => {// done:これで同期でテストできる

						var model = new AppModel();
						
						// どうやらBackboneUndoはタイムスタンプを用いているらしいので同じ時刻にstack.onした場合には認識できない
						setTimeout(() => {
								model.set({ "attr": "attrvalue" });
								assert(model.get("attr") == "attrvalue");
								console.log("attr:" + model.get("attr"));

								setTimeout(() => {
										model.set({
												"item_item": { "item_name": "item_name1", "item_value": "item_value1" }
										});
										assert(model.get("item_item").item_name == "item_name1");
										console.log("item_item.item_name:" + model.get("item_item").item_name);
 
										setTimeout(() => {
												// undo
												model.undoHandler.undo();
												assert(model.get("item_item").item_name == "");
												console.log("item_item.item_name:" + model.get("item_item").item_name);

												setTimeout(() => {
														// redo
														model.undoHandler.redo();
														assert(model.get("item_item").item_value == "item_value1");
														console.log("item_item.item_value:" + model.get("item_item").item_value);

														done(); // これで同期でテストできる

												}, 100);

										}, 100);

								}, 100);


						}, 100);

				});

		});
}

done()を入れることにより、同期をとったテストを実行できます

  • app-test.html



		
		drawing test
		


		

本来ならば、gruntとphantom.jsを用いてコマンドラインから実行するのが普通でしょうが、このapp-test.htmlをブラウザで開くだけでもテストを実行できます。エラーなどはchromeのデバッグツールで確認して下さい

サンプルはこちらにアップしておきました

https://github.com/anagotan/typescript_sample/tree/master/undo

Backbone.UndoをTypescriptで使ってみた その2

前回のサンプルではModelとView辺がうまく分離できていなかったので修正してみた

Backbone.Modelに状態を保存し、イベントを拾って、Undoなどの処理を行うようにしてみた

  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");
		export class App {
				private listview: ItemListView;
				private appModel: AppModel;
				constructor() {
						this.appModel = new AppModel();
						this.listview = new ItemListView({
								"collection":this.appModel.get("collection")
						});
						var self = this;

						undoButton.click(function () {
								console.log("undoButton.click");
								self.appModel.set({ "undoButtonClicked": true });
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								self.appModel.set({ "redoButtonClicked": true });
						});

						addButton.click(function () {
								console.log("addButton.click");
								self.appModel.set({ "addButtonClicked": true });
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								self.appModel.set({ "updateNameButtonClicked": true });
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								self.appModel.set({ "updateValueButtonClicked": true });
						});

				}

				draw(): void {

						this.listview.render();
				}
		}

		class AppModel extends Backbone.Model {
				private undoHandler: UndoHandler;

				constructor(options?) {
						super(options);
						this.undoHandler = new UndoHandler(this.get("collection"));
						this.bind("change", this.change);
				}
				defaults() {
						return {
								collection: new ItemCollection([
										new ItemModel({ "name": "0", "value": "0" }),
								]),
								undoButtonClicked: false,
								redoButtonClicked: false,
								addButtonClicked: false,
								updateNameButtonClicked: false,
								updateValueButtonClicked:false
						}
				}

				change(): void {
						if (this.get("undoButtonClicked")) {
								this.undoHandler.undo();
								this.set({ "undoButtonClicked": false });
						}
						if (this.get("redoButtonClicked")) {
								this.undoHandler.redo();
								this.set({ "redoButtonClicked": false });
						}
						if (this.get("addButtonClicked")) {
								this.get("collection").add(new ItemModel({ "name": "0", "value": "0" }));
								this.set({ "addButtonClicked": false });
						}
						if (this.get("updateNameButtonClicked")) {
								this.get("collection").forEach(function (item) {
										item.set("name", _.random(100).toString());
								});
								this.set({ "updateNameButtonClicked": false });
						}
						if (this.get("updateValueButtonClicked")) {
								this.get("collection").forEach(function (item) {
										item.set("value", _.random(100).toString());
								});
								this.set({ "updateValueButtonClicked": false });
						}
						console.log("AppModel.change");
				}

		}

		class UndoHandler {
				private manager: Backbone.UndoManager;
				 constructor(collections:Backbone.Collection) {

						var self = this;
						 self.manager = new Backbone.UndoManager({ register: collections, track: true });

						 self.manager.removeUndoType("change:isChanging");
						 self.manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});

						 self.manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						 self.manager.trigger("undo redo");

						 self.manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

				 }
				undo(): void{
						this.manager.undo(true);
				}
				redo(): void {
						this.manager.redo(true);
				}
		}

		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
						return {
								"id": _.uniqueId(),
								"name": "",
								"value":"" 
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange" 
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
				constructor(options?) {
						this.el = "#item-list";
						if (options) {
								if (options.collection) this.collection = options.collection;
						}
						super(options);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();
  • index.html




		
		




		



AppModelクラスが状態を保存している。このBackboneModelに対し、各マウスイベントで発生したClickイベントの関数からModelに対し値をセット。Backboneのchangeイベントが発生するのでそれを拾って処理を行う感じ

Backboneをこのように使うのかどうかは不明。。自己流です

Backbone.UndoをTypescriptで使ってみた

backbone.undo.jsというBackboneJS用のUNDOモジュールが有ります。

http://backbone.undojs.com/

こちらをTypescriptで使用してみました。うまくいったのでサンプルを掲載しておきます

通常、Typescriptで既存のライブラリを使用する際には、Typescript用に型定義ファイルを作成してやる必要が有ります。

世の中にはいい人がいて、たいていのライブラリは型定義されていたりします。

https://github.com/borisyankov/DefinitelyTyped

しかし、今回使用するbackbone.undo.jsはまだこちらでは定義されていませんでしたので、適当に作成してみました。

https://github.com/anagotan/DefinitelyTyped/tree/master/backbone.undo

これを利用してUndo機能を実装してみます。

仕様としては、addボタンを押すと「name=0,value=0」のボタンが追加されていきます。update_nameおよびupdate_valueのボタンでランダムな数値に置き換わります。

これをもとにundoおよびredoを実装してみました。

  • index.html




		
		




		



  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");

		export class App {
				private listview: ItemListView;
				constructor() {

						this.listview = new ItemListView({
								"collection": new ItemCollection([
										new ItemModel({ "name": "0" ,"value":"0"}) ,
								])
						});
						var self = this;
						var manager = new Backbone.UndoManager({ register: [this.listview.collection], track: true });

						manager.removeUndoType("change:isChanging");
						manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});

						manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						manager.trigger("undo redo");

						manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

						undoButton.click(function () {
								console.log("undoButton.click");
								manager.undo(true);
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								manager.redo(true);
						});

						addButton.click(function () {
								console.log("addButton.click");

								self.listview.collection.add(new ItemModel({ "name": "0", "value": "0" }));
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								self.listview.collection.forEach(function (item) {
										item.set("name", _.random(100).toString());
								});
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								self.listview.collection.forEach(function (item) {
										item.set("value", _.random(100).toString());
								});
						});

				}

				draw(): void {

						this.listview.render();
				}
		}

		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
						return {
								"id": _.uniqueId(),
								"name": "",
								"value":"" 
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
						this.on("change", this.onchange, this);
						this.on("add", this.onchange, this);
				}
				onchange(model,value,options):void {
						console.log("ItemCollection.change");
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange" 
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
			 // list: ItemCollection;
				constructor(options?) {
						this.el = "#item-list";
						if (options) {
								if (options.collection) this.collection = options.collection;
						}
						super(options);
						//_.bindAll(this);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();

実行する際には別途,jquery,underscore.js,backbone.js を用意しておく必要が有ります