こんにちわ、GX推進グループの平井です。
はじめに
モノリスな証券システムであるGALAXYに代わる、次世代の証券システムをつくろうということで、マイクロサービスな証券システムであるBIGBANGのプロジェクトが2020年にGX推進グループで始動しました。余力を検証するためには複数の商品が必要ということで、まず投信、そして株式のドメインで開発が行われました。僕は投信と株式の双方のドメインで、注文や約定などの機能を担当しました。今回は投信に絞ってその話をしたいと思います。
機能概要
フロントエンドに提供する機能としては、以下の4つがあります。
- 購入(buy-fund)
- 解約(sell-fund)
- 注文取消(cancel-order)
- 注文取得(get-order)
バックエンドにある機能としては、以下の5つがあります。
- 締め(seal-order)
- 概算連絡(contact-order)
- 約定(contract-order)
- 確定連絡(contact-order)
- 注文中金額取得(get-ordering-price)
機能説明
注文
投信の注文には、購入と解約があります。
解約は売却とほぼ同じ意味です。
投信の注文は株式の注文と異なり、数量だけでなく金額で指定することもできます。
締め
投信の各銘柄には締めというものがあります。
締めは委託会社、銘柄ごとに決まっており、
締め切り時刻より前のものは当日の注文として扱い、
締め切り時刻より後のものは翌営業日の注文として扱います。
概算連絡
締め処理をした銘柄の注文内容を委託会社に連絡します。
このときに使用する単価は前営業日のものです。
約定
委託会社から当日の単価が公開されたら、約定を行います。
このとき、残高や顧客勘定の更新を行います。
確定連絡
当日の単価がわかると、数量と正確な約定金額がわかるので、
それらを含めた注文データを委託会社に連絡します。
システム構成
クラウドにはAWSやGCPなどがありますが、機能の豊富さや使いやすさ、世界的なシェアや社内での実績などからAWSになりました。AWSでプログラムを実行できるサービスとして、EC2、ECS、Lambdaなどがありますが、ぼくは開発の手軽さからLambdaを選択しました。フロントエンドにはWeb APIとして機能を提供したいので、API Gatewayを使いました。API GatewayやLambdaはスケールしやすいので、DBもスケールしやすいDynamoDBを使いました。
Lambdaで標準的にサポートされているプログラミング言語として、Node.js、Python、Ruby、Java、Go、C#がありますが、実行速度や実装の容易さからGoにしました。
といった感じで基本的にはAPI Gateway+Lambda(Go)+DynamoDBの構成にしました。ほかにも顧客勘定の更新にSNS、概算連絡や確定連絡のファイルのアップロード先としてS3、概算連絡を起動するためにEventBridge(旧CloudWatch Events)やStep Functions、約定連絡を起動するためにSQSやStep Functionsも使いました。
プログラム
いくつか抜粋して、投信の購入、解約、約定の3つのプログラムを紹介します。
投信購入
package main import ( "context" "encoding/json" "errors" "io/ioutil" "net/http" "strconv" "time" "./constant" "./util" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/google/uuid" ) const ( minOrderAmount = constant.MinOrderAmount maxOrderAmount = constant.MaxOrderAmount functionId = constant.FunctionId success = constant.Success orderStatus = constant.OrderStatus ) type Request struct { ClientId string `json:"client_id"` BrandId string `json:"brand_id"` OrderType string `json:"order_type"` OrderAmount int `json:"order_amount"` } type Response struct { Result string `json:"result"` } type UnitPrice struct { DscrCd string `json:"dscrCd"` LatestPrice struct { StdDt string `json:"stdDt"` Sell struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"sell"` Buy struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"buy"` } `json:"latestPrice"` } type Dates struct { DscrCd string `json:"dscrCd"` AppearLinks bool `json:"appearLinks"` ApplyDt string `json:"applyDt"` // 基準日 TradeDt string `json:"tradeDt"` // 約定日 ValueDt string `json:"valueDt"` // 受渡日 CloseTm string `json:"closeTm"` // 締切時刻 Stopped bool `json:"stopped"` } type Params struct { OrderId string ClientId string BrandId string OrderType string OrderAmount int OrderDate string OrderTime string ContractPrice int UnitPrice int BaseDate string TradeDate string ValueDate string } type BuyingPowerResponse struct { AccountID string `json:"accountId"` TradeDt string `json:"tradeDt"` ValueDt string `json:"valueDt"` Price int `json:"price"` Hold bool `json:"hold"` Error string `json:"error"` } var config map[string]string func init() { config = util.GetConfig() } func main() { lambda.Start(LambdaHandler) } func LambdaHandler(ctx context.Context, req Request) (Response, error) { return buyFund(req) } /* 投信購入 */ func buyFund(req Request) (Response, error) { u, err := uuid.NewRandom() if err != nil { return Response{Result: "NG"}, errors.New("failed at generating uuid") } orderId := u.String() clientId := req.ClientId brandId := req.BrandId orderType := req.OrderType orderAmount := req.OrderAmount // 注文数量チェック if orderType == "buy-lot" && !checkAmount(orderAmount) { return Response{Result: "NG"}, errors.New("order amount is inadequate") } // 単価取得 unitPrice, err := getUnitPrice(brandId) if err != nil { return Response{Result: "NG"}, errors.New("failed at gettting unit price") } // 約定金額計算 contractPrice, res := calcContractPrice(orderType, orderAmount, unitPrice) if !res { return Response{Result: "NG"}, errors.New("failed at calculating contract price") } // 基準日、約定日、受渡日の取得 baseDate, tradeDate, valueDate, err := getDates(brandId) if err != nil { return Response{Result: "NG"}, errors.New("failed at getting base date, trade date, value date") } // 買付可能チェック if err := checkBuyingPower(clientId, contractPrice, tradeDate, valueDate); err != nil { return Response{Result: "NG"}, err } // 注文登録 if !registerFundOrder(Params{ OrderId: orderId, ClientId: clientId, BrandId: brandId, OrderType: orderType, OrderAmount: orderAmount, BaseDate: baseDate, TradeDate: tradeDate, ValueDate: valueDate, ContractPrice: contractPrice, UnitPrice: unitPrice}) { return Response{Result: "NG"}, errors.New("failed at registering fund order") } return Response{Result: "OK"}, nil } /* 注文数量チェック */ func checkAmount(orderAmount int) bool { return orderAmount >= minOrderAmount && orderAmount <= maxOrderAmount } /* 単価取得 */ func getUnitPrice(brandId string) (int, error) { url := config["api:fund-api:"] + "/fund/" + brandId + "/price/latest" method := "GET" client := &http.Client{} req, err := http.NewRequest(method, url, nil) if err != nil { return -1, err } req.Header.Add("Origin", "null") req.Header.Add("Content-Type", "application/json") res, err := client.Do(req) if err != nil { return -1, err } var unitPrice UnitPrice body, err := ioutil.ReadAll(res.Body) if err != nil { return -1, err } if err := json.Unmarshal(body, &unitPrice); err != nil { return -1, err } return unitPrice.LatestPrice.Buy.Price, nil } /* 約定金額計算 */ func calcContractPrice(orderType string, orderAmount int, unitPrice int) (int, bool) { switch orderType { case "buy-lot": return unitPrice * orderAmount, true case "buy-price": return orderAmount, true default: return 0, false } } /* 基準日、約定日、受渡日の取得 */ func getDates(brandId string) (string, string, string, error) { url := config["api:fund-api:"] + "/fund/" + brandId + "/sales" res, err := http.Get(url) if err != nil { return "", "", "", err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return "", "", "", err } var dates Dates if err := json.Unmarshal(body, &dates); err != nil { return "", "", "", err } return dates.ApplyDt, dates.TradeDt, dates.ValueDt, nil } /* 注文登録 */ func registerFundOrder(params Params) bool { now := time.Now() orderDate := now.Format("2006-01-02") orderTime := now.Format("15:04:05") input := &dynamodb.PutItemInput{ Item: map[string]*dynamodb.AttributeValue{ "order_id": { S: aws.String(params.OrderId), }, "client_id": { S: aws.String(params.ClientId), }, "brand_id": { S: aws.String(params.BrandId), }, "order_type": { S: aws.String(params.OrderType), }, "order_amount": { S: aws.String(strconv.Itoa(params.OrderAmount)), }, "order_status": { S: aws.String(orderStatus), }, "order_date": { S: aws.String(orderDate), }, "order_time": { S: aws.String(orderTime), }, "base_date": { S: aws.String(params.BaseDate), }, "trade_date": { S: aws.String(params.TradeDate), }, "value_date": { S: aws.String(params.ValueDate), }, "unit_price": { S: aws.String(strconv.Itoa(params.UnitPrice)), }, "contract_price": { S: aws.String(strconv.Itoa(params.ContractPrice)), }, "update_date": { S: aws.String(orderDate), }, "update_time": { S: aws.String(orderTime), }, }, TableName: aws.String("fund-order"), } _, err := dynamodb.New(session.New()).PutItem(input) if err != nil { return false } return true } /* 買付可能チェック */ func checkBuyingPower(accountId string, contractPrice int, tradeDt string, valueDt string) error { url := config["api:bp-api:"] + "/buying-power/" + accountId + "?trade_dt=" + tradeDt + "&value_dt=" + valueDt + "&price=" + strconv.Itoa(contractPrice) method := "GET" client := &http.Client{} req, err := http.NewRequest(method, url, nil) if err != nil { return err } req.Header.Add("Origin", "null") req.Header.Add("Content-Type", "application/json") res, err := client.Do(req) if err != nil { return err } var buyingPowerResponse BuyingPowerResponse body, err := ioutil.ReadAll(res.Body) if err != nil { return err } if err := json.Unmarshal(body, &buyingPowerResponse); err != nil { return err } if !buyingPowerResponse.Hold { return errors.New(buyingPowerResponse.Error) } return nil }
投信解約
package main import ( "context" "encoding/json" "errors" "io/ioutil" "net/http" "strconv" "time" "./constant" "./util" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/google/uuid" ) const ( minOrderAmount = constant.MinOrderAmount maxOrderAmount = constant.MaxOrderAmount functionId = constant.FunctionId success = constant.Success orderStatus = constant.OrderStatus ) type Request struct { ClientId string `json:"client_id"` BrandId string `json:"brand_id"` OrderType string `json:"order_type"` OrderAmount int `json:"order_amount"` } type Response struct { Result string `json:"result"` } type UnitPrice struct { DscrCd string `json:"dscrCd"` LatestPrice struct { StdDt string `json:"stdDt"` Sell struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"sell"` Buy struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"buy"` } `json:"latestPrice"` } type Dates struct { DscrCd string `json:"dscrCd"` AppearLinks bool `json:"appearLinks"` ApplyDt string `json:"applyDt"` // 基準日 TradeDt string `json:"tradeDt"` // 約定日 ValueDt string `json:"valueDt"` // 受渡日 CloseTm string `json:"closeTm"` // 締切時刻 Stopped bool `json:"stopped"` } type Params struct { OrderId string ClientId string BrandId string OrderType string OrderAmount int OrderDate string OrderTime string ContractPrice int UnitPrice int BaseDate string TradeDate string ValueDate string } type BalanceEntity []struct { AccountID int `json:"accountId"` BranchCd int `json:"branchCd"` ClientCd int `json:"clientCd"` DscrCd string `json:"dscrCd"` GeneralAccmInvKbn int `json:"generalAccmInvKbn"` SpAccKbn int `json:"spAccKbn"` SubGuarantyKbn int `json:"subGuarantyKbn"` TodayTdOpenNominal int `json:"todayTdOpenNominal"` TodayStdDt string `json:"todayStdDt"` FrontUpdTm string `json:"frontUpdTm"` DoFoClassKbn int `json:"doFoClassKbn"` } var config map[string]string func init() { config = util.GetConfig() } func main() { lambda.Start(LambdaHandler) } func LambdaHandler(ctx context.Context, req Request) (Response, error) { return sellFund(req) } /* 投信解約 */ func sellFund(req Request) (Response, error) { u, err := uuid.NewRandom() if err != nil { return Response{Result: "NG"}, errors.New("failed at generating uuid") } orderId := u.String() clientId := req.ClientId brandId := req.BrandId orderType := req.OrderType orderAmount := req.OrderAmount // 注文数量チェック if !checkAmount(orderAmount) { return Response{Result: "NG"}, errors.New("order amount is inadequate") } // 残高チェック if !checkBalance(clientId, brandId, orderAmount) { return Response{Result: "NG"}, errors.New("error at checking balance") } // 単価取得 unitPrice, status := getUnitPrice(brandId) if !status { return Response{Result: "NG"}, errors.New("failed at gettting unit price") } // 約定金額計算 contractPrice, status := calcContractPrice(orderType, orderAmount, unitPrice) if !status { return Response{Result: "NG"}, errors.New("failed at calculating contract price") } // 基準日、約定日、受渡日の取得 baseDate, tradeDate, valueDate, err := getDates(brandId) if err != nil { return Response{Result: "NG"}, errors.New("failed at getting base date, trade date, value date") } // 注文登録 if !registerFundOrder(Params{ OrderId: orderId, ClientId: clientId, BrandId: brandId, OrderType: orderType, OrderAmount: orderAmount, BaseDate: baseDate, TradeDate: tradeDate, ValueDate: valueDate, ContractPrice: contractPrice, UnitPrice: unitPrice}) { return Response{Result: "NG"}, errors.New("failed at registering fund order") } return Response{Result: "OK"}, nil } /* 注文数量チェック */ func checkAmount(orderAmount int) bool { return orderAmount >= minOrderAmount && orderAmount <= maxOrderAmount } /* 残高チェック */ func checkBalance(clientId string, brandId string, orderAmount int) bool { url := config["api:fund-api:"] + "/balance/" + clientId + "/" + brandId response, err := http.Get(url) if err != nil { return false } defer response.Body.Close() byteBody, err := ioutil.ReadAll(response.Body) if err != nil { return false } var structBody BalanceEntity if err := json.Unmarshal(byteBody, &structBody); err != nil { return false } if structBody[0].TodayTdOpenNominal >= orderAmount { return true } return false } /* 単価取得 */ func getUnitPrice(brandId string) (int, bool) { url := config["api:fund-api:"] + "/fund/" + brandId + "/price/latest" response, err := http.Get(url) if err != nil { return 0, false } defer response.Body.Close() byteBody, err := ioutil.ReadAll(response.Body) if err != nil { return 0, false } var structBody UnitPrice if err := json.Unmarshal(byteBody, &structBody); err != nil { return 0, false } return structBody.LatestPrice.Buy.Price, true } /* 約定金額計算 */ func calcContractPrice(orderType string, orderAmount int, unitPrice int) (int, bool) { switch orderType { case "sell-lot": return unitPrice * orderAmount, true case "sell-price": return orderAmount, true default: return 0, false } } /* 基準日、約定日、受渡日の取得 */ func getDates(brandId string) (string, string, string, error) { url := config["api:fund-api:"] + "/fund/" + brandId + "/sales" res, err := http.Get(url) if err != nil { return "", "", "", err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return "", "", "", err } var dates Dates if err := json.Unmarshal(body, &dates); err != nil { return "", "", "", err } return dates.ApplyDt, dates.TradeDt, dates.ValueDt, nil } /* 注文登録 */ func registerFundOrder(params Params) bool { now := time.Now() orderDate := now.Format("2006-01-02") orderTime := now.Format("15:04:05") input := &dynamodb.PutItemInput{ Item: map[string]*dynamodb.AttributeValue{ "order_id": { S: aws.String(params.OrderId), }, "client_id": { S: aws.String(params.ClientId), }, "brand_id": { S: aws.String(params.BrandId), }, "order_type": { S: aws.String(params.OrderType), }, "order_amount": { S: aws.String(strconv.Itoa(params.OrderAmount)), }, "order_status": { S: aws.String(orderStatus), }, "order_date": { S: aws.String(orderDate), }, "order_time": { S: aws.String(orderTime), }, "base_date": { S: aws.String(params.BaseDate), }, "trade_date": { S: aws.String(params.TradeDate), }, "value_date": { S: aws.String(params.ValueDate), }, "unit_price": { S: aws.String(strconv.Itoa(params.UnitPrice)), }, "contract_price": { S: aws.String(strconv.Itoa(params.ContractPrice)), }, "update_date": { S: aws.String(orderDate), }, "update_time": { S: aws.String(orderTime), }, }, TableName: aws.String("fund-order"), } _, err := dynamodb.New(session.New()).PutItem(input) if err != nil { return false } return true }
約定
package main import ( "bytes" "context" "encoding/json" "errors" "io/ioutil" "net/http" "strconv" "strings" "time" "./util" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/sns" ) const ( // 投資区分 GENERAL_TYPE = 1 // 一般型 ACCUMULATED_TYPE = 2 // 累投型 // 口座区分 ACC_TYPE_NOT_SET = 0 // 未設定 ACC_TYPE_SPECIAL = 1 // 特定口座 ACC_TYPE_GENERAL = 2 // 一般口座 ACC_TYPE_TAX_FREE = 3 // 非課税口座 // 代用差入先区分 GUARANTEE_TYPE_NOT_SET = 0 // 未設定 GUARANTEE_TYPE_NORMAL = 1 // 普通預り GUARANTEE_TYPE_MARGIN = 2 // 信用保証金代用 GUARANTEE_TYPE_DEPOSIT = 4 // 証拠金代用 // 取引区分 TRADE_TYPE_SELL = 1 // 解約 TRADE_TYPE_BUY = 2 // 購入 TRADE_TYPE_CANCEL = 3 // 取消 ) type Request struct { BrandIds []string `json:"brand_ids"` TradeDate string `json:"trade_date"` } type Response struct { Result string `json:"result"` } type Order struct { OrderId string `dynamodbav:"order_id"` ClientId string `dynamodbav:"client_id"` BrandId string `dynamodbav:"brand_id"` OrderType string `dynamodbav:"order_type"` OrderAmount int `dynamodbav:"order_amount"` OrderStatus string `dynamodbav:"order_status"` UnitPrice int `dynamodbav:"unit_price"` ExecutionPrice int `dynamodbav:"execution_price"` CreateDatetime string `dynamodbav:"create_datetime"` UpdateDatetime string `dynamodbav:"update_datetime"` } type UnitPrice struct { DscrCd string `json:"dscrCd"` LatestPrice struct { StdDt string `json:"stdDt"` Sell struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"sell"` Buy struct { Price int `json:"price"` Diff int `json:"diff"` } `json:"buy"` } `json:"latestPrice"` } type UnitPricePair struct { BuyPrice int `json:"buy_price"` SellPrice int `json:"sell_price"` } type BalanceParameter struct { AccountID int `json:"accountId"` // BranchCd int `json:"branchCd"` // ClientCd int `json:"clientCd"` DscrCd string `json:"dscrCd"` GeneralAccmInvKbn int `json:"generalAccmInvKbn"` SpAccKbn int `json:"spAccKbn"` SubGuarantyKbn int `json:"subGuarantyKbn"` TradeTypeCd int `json:"tradeTypeCd"` Nominal int `json:"nominal"` TradeDt string `json:"tradeDt"` NameDt string `json:"nameDt"` ExecutionPrice int `json:"executionPrice"` } type ContactOrderParameter struct { BrandIds []string `json:"brand_ids"` ContactType string `json:"contact_type"` } type LedgerUpdateParameter struct { UID string `json:"uid"` Product string `json:"product"` AccountId string `json:"accountId"` ValueDt string `json:"valueDt"` Deposit int `json:"deposit"` Sell bool `json:"sell"` } var config map[string]string func init() { config = util.GetConfig() } func main() { lambda.Start(LambdaHandler) } func LambdaHandler(ctx context.Context, req Request) (Response, error) { return execute(req) } func execute(req Request) (Response, error) { // 単価取得 brandId2UnitPrice, err := getUnitPrices(req.BrandIds) if err != nil { return Response{Result: "NG"}, err } // 約定処理+残高更新+客勘更新 err = contractOrder(req.BrandIds, req.TradeDate, brandId2UnitPrice) if err != nil { return Response{Result: "NG"}, err } return Response{Result: "OK"}, nil } /* 単価取得 */ func getUnitPrices(brandIds []string) (map[string]UnitPricePair, error) { brandId2UnitPrice := make(map[string]UnitPricePair) var unitPrices []UnitPrice for _, brandId := range brandIds { url := config["api:fund-api:"] + "/fund/" + brandId + "/price/latest" method := "GET" client := &http.Client{} req, err := http.NewRequest(method, url, nil) if err != nil { return brandId2UnitPrice, err } req.Header.Add("Origin", "null") req.Header.Add("Content-Type", "application/json") res, err := client.Do(req) if err != nil { return brandId2UnitPrice, err } var unitPrice UnitPrice body, err := ioutil.ReadAll(res.Body) if err != nil { return brandId2UnitPrice, err } if err := json.Unmarshal(body, &unitPrice); err != nil { return brandId2UnitPrice, err } unitPrices = append(unitPrices, unitPrice) } for _, unitPrice := range unitPrices { brandId2UnitPrice[unitPrice.DscrCd] = UnitPricePair{unitPrice.LatestPrice.Buy.Price, unitPrice.LatestPrice.Sell.Price} } return brandId2UnitPrice, nil } /* 約定処理+残高更新+客勘更新 */ func contractOrder(brandIds []string, tradeDate string, brandId2UnitPrice map[string]UnitPricePair) error { for _, brandId := range brandIds { // 約定対象のアイテム抽出 results, err := dynamodb.New(session.New()).Scan(&dynamodb.ScanInput{ TableName: aws.String("fund-order"), ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":brand_id": { S: aws.String(brandId), }, ":trade_date": { S: aws.String(tradeDate), }, ":order_status": { S: aws.String("ordered"), }, }, FilterExpression: aws.String("brand_id = :brand_id AND trade_date = :trade_date AND order_status = :order_status"), }) if err != nil { return err } for _, item := range results.Items { orderAmount, err := strconv.Atoi(*item["order_amount"].S) if err != nil { return err } // 単価 var unitPrice int if strings.Contains(*item["order_type"].S, "buy") { unitPrice = brandId2UnitPrice[brandId].BuyPrice } else if strings.Contains(*item["order_type"].S, "sell") { unitPrice = brandId2UnitPrice[brandId].SellPrice } // 約定金額と数量 var contractPrice, nominal int // 金額指定の場合 if strings.Contains(*item["order_type"].S, "price") { contractPrice = orderAmount nominal = orderAmount * 10000 / unitPrice // 口数指定の場合 } else { contractPrice = unitPrice * orderAmount nominal = orderAmount * 10000 } now := time.Now() date := now.Format("2006-01-02") time := now.Format("15:04:05") // 注文ステータスの更新および単価と約定金額の追加 _, err = dynamodb.New(session.New()).PutItem(&dynamodb.PutItemInput{ TableName: aws.String("fund-order"), Item: map[string]*dynamodb.AttributeValue{ "order_id": { S: aws.String(*item["order_id"].S), }, "client_id": { S: aws.String(*item["client_id"].S), }, "brand_id": { S: aws.String(*item["brand_id"].S), }, "order_type": { S: aws.String(*item["order_type"].S), }, "order_amount": { S: aws.String(*item["order_amount"].S), }, "order_status": { S: aws.String("contracted"), }, "order_date": { S: aws.String(*item["order_date"].S), }, "order_time": { S: aws.String(*item["order_time"].S), }, "base_date": { S: aws.String(*item["base_date"].S), }, "trade_date": { S: aws.String(*item["trade_date"].S), }, "value_date": { S: aws.String(*item["value_date"].S), }, "update_date": { S: aws.String(date), }, "update_time": { S: aws.String(time), }, "unit_price": { S: aws.String(strconv.Itoa(unitPrice)), }, "contract_price": { S: aws.String(strconv.Itoa(contractPrice)), }, }, }) if err != nil { return err } // 残高更新 if !updateBalance(item, contractPrice, nominal) { return errors.New("failed at updateBalance") } // 客勘更新 if !updateLedger(item, contractPrice) { return errors.New("failed at updateLedger") } } } return nil } /* 残高更新 */ func updateBalance(item map[string]*dynamodb.AttributeValue, contractPrice int, nominal int) bool { // データの設定 url := config["api:fund-api:"] + "/balance" method := "POST" var tradeType int if strings.Contains(*item["order_type"].S, "buy") { tradeType = TRADE_TYPE_BUY } else if strings.Contains(*item["order_type"].S, "sell") { tradeType = TRADE_TYPE_SELL } accountId, err := strconv.Atoi(*item["client_id"].S) if err != nil { return false } data, err := json.Marshal(BalanceParameter{ AccountID: accountId, DscrCd: *item["brand_id"].S, GeneralAccmInvKbn: ACCUMULATED_TYPE, SpAccKbn: ACC_TYPE_SPECIAL, SubGuarantyKbn: GUARANTEE_TYPE_NORMAL, TradeTypeCd: tradeType, Nominal: nominal, NameDt: *item["value_date"].S, TradeDt: *item["trade_date"].S, ExecutionPrice: contractPrice, }) // HTTPリクエスト client := &http.Client{} req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) if err != nil { return false } req.Header.Add("Origin", "null") req.Header.Add("Content-Type", "application/json") _, err = client.Do(req) if err != nil { return false } return true } /* 客勘更新(客勘=顧客勘定、元帳とも呼ばれる) */ func updateLedger(item map[string]*dynamodb.AttributeValue, contractPrice int) bool { var tradeType bool if strings.Contains(*item["order_type"].S, "buy") { tradeType = false } else if strings.Contains(*item["order_type"].S, "sell") { tradeType = true } sess, err := session.NewSession(&aws.Config{ Region: aws.String(config["aws:region:"]), }) if err != nil { return false } svc := sns.New(sess) message, err := json.Marshal(LedgerUpdateParameter{ UID: *item["order_id"].S, Product: "fund", AccountId: *item["client_id"].S, ValueDt: *item["value_date"].S, Deposit: contractPrice, Sell: tradeType, }) if err != nil { return false } input := &sns.PublishInput{ Message: aws.String(string(message)), TopicArn: aws.String(config["topic:ledger-update:"]), } _, err = svc.Publish(input) if err != nil { return false } return true }
購入と解約はほぼ同じで、相違点は購入では余力チェック(買付可能かどうかのチェック)、解約では残高チェックがあるといったところです。約定では購入の場合はお金を減らして残高を増やし、逆に解約ではお金を増やして残高を減らすといったことをします。
ビルドおよびデプロイなど
Lambdaの関数ごとにディレクトリを分けていて、各ディレクトリにはbuild.sh、deploy.sh、run.shの3つを置きました。
build.sh
#!/bin/bash goimports -w main.go go fmt GOOS=linux go build main.go
- goimportsはソースコードの内容に応じてimport文を追加、削除、整列してくれるツールです。goimportsを使えば人間はimport文について何も考えなくて済みます(ちなみにですがGoでは使われていないパッケージまでimportしているとコンパイル時にエラーになります)。go getで簡単にインストールして使えます。
- go fmtはソースコードをフォーマット(整形)してくれるツールです。これはgoコマンドにある機能で、goをインストールすれば使えます。
- AWS LambdaのランタイムOSはプログラミング言語とそのバージョンの組み合わせによって異なりますが、Amazon LinuxまたはAmazon Linux 2のどちらかなのでLinux用にビルドする必要があり、そのためにはシェル変数GOOSの値をlinuxにしてビルドする必要があります。
deploy.sh
#!/bin/bash zip -rq func.zip . aws --profile fund lambda update-function-code --function-name buy-fund --zip-file fileb://func.zip rm func.zip rm main
デプロイはzipファイルにまとめてLambdaの関数にアップロードするだけです。更新するドメインのアカウントに応じてプロファイル名、Lambda関数に応じてLambda関数名をそれぞれ指定する感じです。
run.sh
#!/bin/bash
./build.sh && ./deploy.sh
run.shを実行するとbuild.shとdeploy.shを実行する仕組みです。build.shが成功したときだけdeploy.shを実行するようにしています。こうすることでビルドが失敗したものをアップロードするということがなくなります。テストについて今回はAWSの画面上で手動テストしていたのですが、ロジックの単体テストを書いて、ビルドとデプロイの間でテスト(単体テスト)をするようにするとなお良いですよね〜。以下、理想。
#!/bin/bash ./build.sh && ./test.sh && ./deploy.sh
まとめ
今回はAPI Gateway+Lambda(Go)+DynamoDBでの開発例を紹介しました。Goの魅力のひとつとして「プログラムの実行速度が高速」などが有名ですが、それ以外にもGoコマンドやGoによるCLIツールが便利だということを知ってもらえたら幸いです。それではまた。