|
Example 5 : pump web data into your database
Use case : you had a look at Bitstamp's bitcoin order book, you noticed that they have a public web api with json output, and you want to pump this data into your Postgres database for some simple analysis.
The output produced by GET https://www.bitstamp.net/api/order_book looks like this:
{ "timestamp": "1398533793",
"bids": [["463.07", "0.12700000"], ["463.06", "1.85985919"], ["462.50",..] ..],
"asks": [["463.80", "7.50530248"], ["464.45", "0.21700000"], ["464.80", ..]..]
}
The following amalegeni-go code shows you how to easily import this data into a database with 3 tables, and how to produce some simple statistics on the data, like :
20140427 09:50 order:1009
Bid: vol:10355710 num:2577 max:456.03
Ask: vol:17426269 num:5236 min:458.60
Vol-Ratio: 1.68
In case your trading knowledge needs brushing up:
- the bid price is the maximum price a buyer is willing to pay
- the ask price is the minimum price a seller is willing to sell his bitcoins at.
Source code
For detailed explanation of the pre-requisites and how to setup the database, please look at the prior examples. You are now expected to be able to hit the ground running with amgo !
Download and unpack ex05.zip that contains the code and data.
Create the tables
First the 'drop the tables' program, because during development I experiment a lot with changing the structure of the tables, creating other indexes, changing field names,....
Program: drop_tables.amg
1
2
3
4
| func D01() << drop table if exists t_bid >> run
func D02() << drop table if exists t_ask >> run
func D03() << drop table if exists t_order >> run
func D04() << drop sequence seq_order_id >> run
|
And then here is the program to create the t_order, t_bid and t_ask table. If any of the tables already exists, the program bails out!
You've spotted the the 'run' appended to each function definition, this tells the amgo-template that this function needs executing. This behaviour is only implemented as such for the 'gq' template. The behaviour for the 's' and 'se' template (see other examples) is different: the run indicates which functions should have test-code generated.
Program: create_tables.amg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| import func ( "os" )
func CheckFirst() {{
if (CountTable( " 't_order','t_bid','t_ask' " ) > 0 ) {
fmt.Println("Cannot create, first drop existing tables!")
os.Exit(1)
}
}}
run
func CountTable( tableInList injection ) int <<
select count(1)
from pg_tables
where tablename in ( tableInList )
>>
func C01() <<
create sequence seq_order_id start with 1000 increment by 1 cache 1
>> run
func C02() <<
create table t_order (
order_id integer primary key default nextval('seq_order_id')
, creation_time timestamp default current_timestamp
, bitstamp_timestamp integer
)
>> run
func C03() <<
create table t_bid (
order_id integer references t_order(order_id)
, price numeric
, amount numeric
)
>> run
func C04() <<
create table t_ask (
order_id integer references t_order(order_id)
, price numeric
, amount numeric
)
>> run
func C05() <<
create index i_ask_d1 on t_ask (order_id)
>> run
func C06() <<
create index i_bid_d1 on t_bid(order_id)
>> run
func CheckAfter() {{
if (CountTable( " 't_order','t_bid','t_ask' " ) == 3 ) {
fmt.Println("All three tables succesfully created!")
} else {
fmt.Println("Something went wrong, not all three tables exist! Not good...")
}
}}
run
|
Compile the above programs:
$ cd orderbook
$ . ./setenv.sh
$ amgo drop_tables.amg
template/gq.tpl * drop_tables.amg
Generating: src/drop_tables/main_generated.go
Generating: src/drop_tables/main_generated_test.go
$ go install drop_tables
$ amgo create_tables.amg
template/gq.tpl * create_tables.amg
Generating: src/create_tables/main_generated.go
Generating: src/create_tables/main_generated_test.go
$ go install create_tables
.. and run ..
$ create_tables
All three tables succesfully created!
Grab data
The following 'grab' program uses a http GET to get the json data from the bitstamp site.
Since the data has all the numbers surrounded with double-quotes (see sample data at the top of this page), and we want Go's JSON unmarshaller to immediately convert to the right format, we first apply a regular expression hack on the received JSON data: it removes the double quotes from every number in this string, but leaves the double quotes round labels like "timestamp" intact.
The JSON data is then 'unmarshalled' into the OrderBook object, whose data will be cherry-picked to send to the database.
Since inserting the bids/aks one by one will take too much time, there are a few thousands of them, we first append the bids and asks into a slice, and then batch insert the slice.
Finally, a count is done of how many orders (always 1), and its corresponding bids (a few thousand) and asks (also a few thousand) have been inserted into the database.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
import func (
"encoding/json"
"io/ioutil"
"regexp"
"net/http"
)
func Grab()
{{
// get the data via HTTP GET from bitstamp
resp, err := http.Get("https://www.bitstamp.net/api/order_book/")
if err != nil {
panic(err)
}
defer resp.Body.Close()
rawdata, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
// cleanup the Json data: turn every double quoted number into an unquoted number
re := regexp.MustCompile( `"([0-9.][^"]*)"` )
data:= re.ReplaceAll(rawdata, []byte("$1"))
// turn the cleansed Json data into the OrderBook object
var ob OrderBook
err=json.Unmarshal( data , &ob)
if (err!=nil) {
panic(err)
}
// start inserting the data into the db
id:=NextSeq()
fmt.Println("* Insert order")
InsertOrder(id, ob.Timestamp )
// convert the bids into a PriceAmount slice for insertion
bidSl:=make([]PriceAmount,0,0)
for _,bid := range( ob.Bids ) {
bidSl=append(bidSl, PriceAmount{ Id: id, Price: bid[0], Amount:bid[1] } )
}
fmt.Println("* Insert bids")
InsertBid(bidSl)
// convert the asks into a PriceAmount slice for insertion
askSl:=make([]PriceAmount,0,0)
for _,ask:= range( ob.Asks) {
askSl=append(askSl, PriceAmount{ Id: id, Price: ask[0], Amount:ask[1] } )
}
fmt.Println("* Insert asks")
InsertAsk(askSl)
fmt.Printf("Inserted order id %d: %v.\n", id, Count(id))
}}
type OrderBook struct {
Timestamp int64
Bids [][]float32
Asks [][]float32
}
type PriceAmount struct {
Id int
Price float32
Amount float32
}
func NextSeq() int <<
select nextval('seq_order_id')
>>
func InsertOrder( id int, tst int64 ) <<
insert into t_order( order_id, bitstamp_timestamp ) values( $1, $2 )
>>
func InsertAsk( sl []PriceAmount) <<
insert into t_ask( order_id, price, amount ) values( $1, $2, $3 )
>>
func InsertBid( sl []PriceAmount) <<
insert into t_bid( order_id, price, amount ) values( $1, $2, $3 )
>>
func Count( id int ) []string <<
select to_char(count(1),'999') || ' order ' from t_order where order_id=$1
union all
select to_char(count(1),'999,999') || ' asks' from t_ask where order_id=$1
union all
select to_char(count(1),'999,999') || ' bids' from t_bid where order_id=$1
>>
|
Compile it, and try it ...
$ amgo grab.amg
template/gq.tpl * grab.amg
Generating: src/grab/main_generated.go
Generating: src/grab/main_generated_test.go
$ go install grab
$ grab
* Insert order
* Insert bids
* Insert asks
Inserted order id 1000: [ 1 order 5,703 asks 2,518 bids].
If all is hunky dory, then add it to your crontab to execute a data grab every 10 minutes.
$ crontab -e
# m h dom mon dow command
0,10,20,30,40,50 * * * * /home/willem/orderbook/bin/grab >> /home/willem/orderbook/grab.log 2>&1
And while the above is kicking off every 10 minutes YOU should go for a run. Recommended distance: 10km. In other words: come back in an hour.
Some simple statistics
(An hour later)
First of all: before your database is filling up to the rafters, maybe kill the above cronjob.
To run some queries on the accumulated data, here's the program stat.amg :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
func SimpleStats() {{
for _,id := range(OrderList()) {
bidvol:=BidVolume(id)
askvol:=AskVolume(id)
fmt.Printf("\n%s\torder:%d\n",OrderTst(id),id)
fmt.Printf("\tBid:\tvol:%-9.0f\tnum:%d\tmax:%-9.2f\n",
bidvol,
BidCount(id),
BidMaxPrice(id) )
fmt.Printf("\tAsk:\tvol:%-9.0f\tnum:%d\tmin:%-9.2f\n",
askvol,
AskCount(id),
AskMinPrice(id) )
if (bidvol!=0.0) {
ratio:=askvol/bidvol
fmt.Printf("\tVol-Ratio:\t%-9.2f\n",ratio)
}
}
}} run
func OrderList() []int <<
select order_id
from t_order
order by 1
>>
func OrderTst(id int) string <<
select to_char(creation_time,'YYYYMMDD HH24:MI') ct
from t_order
where order_id=$1
>>
func AskCount( id int ) int <<
select count(1) from t_ask where order_id=$1
>>
func AskVolume(id int) float64 <<
select sum(price*amount)
from t_ask
where order_id=$1
>>
func AskMinPrice(id int) float64 <<
select min(price)
from t_ask
where order_id=$1
>>
func BidCount( id int ) int <<
select count(1) from t_bid where order_id=$1
>>
func BidVolume(id int) float64 <<
select sum(price*amount)
from t_bid
where order_id=$1
>>
func BidMaxPrice(id int) float64 <<
select max(price)
from t_bid
where order_id=$1
>>
|
Compile as you did in previous examples. Then run:
$ stat
..
..
20140427 08:21 order:1000
Bid: vol:10357729 num:2583 max:455.74
Ask: vol:17459959 num:5429 min:456.13
Vol-Ratio: 1.69
20140427 08:30 order:1001
Bid: vol:10370215 num:2581 max:456.13
Ask: vol:17456737 num:5420 min:457.76
Vol-Ratio: 1.68
20140427 08:40 order:1002
Bid: vol:10352870 num:2581 max:456.11
Ask: vol:17393255 num:5421 min:458.10
Vol-Ratio: 1.68
20140427 08:50 order:1003
Bid: vol:10373219 num:2576 max:456.11
Ask: vol:17424294 num:5378 min:458.10
Vol-Ratio: 1.68
20140427 09:00 order:1004
Bid: vol:10368069 num:2576 max:456.11
Ask: vol:17357831 num:5239 min:458.10
Vol-Ratio: 1.67
| |