最近这几个月,我和我们团队都在忙一个外汇交易所的项目,客户是一个做全球外汇交易的机构,主攻的是非洲、中东这些新兴市场。他们找到我们时,明确说不想从零开发,而是希望我们基于已有的开源交易所系统来深度定制,快速上线。
这是我第一次从头到尾跟进一个交易平台的落地过程,从源码调研、部署,到深度改造、对接支付和做KYC,确实遇到不少坑,也收获不少经验,今天在这篇文章里,和大家聊聊其中的一些故事。
找到一套靠谱的交易所源码
我们一开始就在调研现成的交易所方案,说实话,GitHub 上那种Crypto exchange in 100 lines of code的项目不少,但真正能拿来商用的很少。最后我们选的是一套用 Go 写的交易所核心 + 前端是 Vue 的框架,功能还算完整:撮合引擎、撮合订单、行情推送、WebSocket 实时数据、币种管理这些基本都有,虽然是给加密货币做的,但整体结构通用,适合改成外汇。
我们做的第一步是适配币种 —— 变成外汇交易所
加密货币的撮合是撮数量,但外汇是按金额+杠杆来的,客户也不关心限价单、市价单这些细节,他们只想做多和空的交易方式,看起来更像CFD平台。于是我们在撮合引擎的核心加了一层 wrapper:
// 新增:外汇交易单结构体
type ForexOrder struct {
Direction string // buy or sell
Amount float64 // 入金金额
Leverage int // 杠杆倍数
Symbol string // 交易对
}
// 根据方向换算成市价下单
func ConvertToMarketOrder(order ForexOrder) *Order {
var side string
if order.Direction == "buy" {
side = "bid"
} else {
side = "ask"
}
volume := order.Amount * float64(order.Leverage) // 外汇按杠杆计算下单量
return &Order{
Side: side,
Symbol: order.Symbol,
Volume: volume,
Type: "market",
}
}
我们保留了撮合引擎的核心逻辑,但对接层变成了CFD风格,交易方式对用户更友好。
客户说要会员系统,而且是分等级的
他们希望平台里有不同等级的会员,比如普通会员、银卡、金卡,手续费不一样,最大杠杆也不一样。于是我们给用户模型加了会员等级字段,并在风控模块做了判断:
type User struct {
ID int64
Email string
MemberLevel string // 普通、银卡、金卡
}
func GetUserLeverageLimit(level string) int {
switch level {
case "普通":
return 20
case "银卡":
return 50
case "金卡":
return 100
default:
return 10
}
}
每次用户下单前,我们都从用户等级里提取最大杠杆,超过的就拦截,前端也会实时显示当前剩余额度。
接入行情数据
客户在前期需求沟通的时候,有说过他们需要接入实时外汇行情,一定不能是延时的。当时我也不太懂里面的区别,就没太在意。等做到行情这块的时候,客户主动说他们已经定好了供应商,他们指定使用infoway API的数据,我看了他们的文档都还挺清晰,对接也不难,经过一下午的开发(中间还摸了会鱼),我们完成了从认证到数据解析的全链路接入。这里分享关键代码模块:
// Infoway 外汇行情 WebSocket 接入模块
package marketdata
import (
"encoding/json"
"log"
"net/url"
"time"
"github.com/gorilla/websocket"
)
const (
apiKey = "YourAPIKey" // 在https://infoway.io注册后获取
symbolCode = "EURUSD" // 外汇交易对
)
func StartForexMarketFeed() {
u := url.URL{
Scheme: "wss",
Host: "data.infoway.io",
Path: "/ws",
RawQuery: "business=forex&apikey=" + apiKey,
}
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("WebSocket dial error:", err)
}
defer conn.Close()
// 发送订阅消息
subMsg := map[string]interface{}{
"code": 10000,
"trace": "forex-feed-trace-id-001",
"data": map[string]string{
"codes": symbolCode,
},
}
msgBytes, _ := json.Marshal(subMsg)
if err := conn.WriteMessage(websocket.TextMessage, msgBytes); err != nil {
log.Fatal("Subscription error:", err)
}
// 启动 ping 定时器
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
go func() {
for range ticker.C {
ping := map[string]interface{}{
"code": 10010,
"trace": "ping-forex-feed",
}
pingBytes, _ := json.Marshal(ping)
if err := conn.WriteMessage(websocket.TextMessage, pingBytes); err != nil {
log.Println("Ping error:", err)
return
}
}
}()
// 接收行情消息
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
return
}
log.Printf("Received Forex Tick: %s", message)
// TODO: 将行情解析后写入缓存或广播到前端
}
}
接入当地支付方式
因为客户做的是非洲市场,主流支付不是 Visa/Master,而是当地钱包,比如 M-Pesa和 Flutterwave。所以我们不得不写了一堆支付适配器,每个都像一个 mini-SDK。
以 Flutterwave 为例,我们封装了统一的充值接口:
func CreateFlutterwavePayment(userID int64, amount float64, currency string) (string, error) {
data := map[string]interface{}{
"tx_ref": fmt.Sprintf("tx-%d-%d", userID, time.Now().Unix()),
"amount": amount,
"currency": currency,
"redirect_url": "https://example.com/payment/callback",
"customer": map[string]string{
"email": "user@example.com",
},
}
// 发送POST请求到Flutterwave的支付接口
resp, err := http.PostJson("https://api.flutterwave.com/v3/payments", data)
if err != nil {
return "", err
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result["data"].(map[string]interface{})["link"].(string), nil
}
这个接口会返回一个跳转链接,用户打开后完成支付,我们再根据回调更新数据库状态。
做完这些之后我最大的感受是:**一个交易所不是一个技术项目,它更像是一个运营系统,**你必须时刻想客户会怎么用、用户会在哪卡住、你是不是能让前端少几个按钮,后台少几次操作。
这次项目我们从源码出发,逐步迭代出一个客户满意的外汇交易平台,也留下了很多可复用的模块——无论是行情数据、支付SDK、风控系统,甚至未来对接MT5/MT4这些交易网关,我们已经做好了准备。
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章