Amalegeni-Go
 

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     
 
© Willem Moors, 2013 - 2020