자동화 구축: Docker 이미지를 빌드, 푸시, 재시작하는 GitHub Actions 워크플로 추가 및 고도화된 매수 체결 로직 반영
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 12s
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 12s
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -26,7 +27,7 @@ const (
|
||||
cooldownMinutes = 5 // 동일 종목 재진입 쿨다운(분)
|
||||
exitLoopSec = 5 // 청산 루프 주기(초)
|
||||
entryLoopSec = 10 // 진입 루프 주기(초)
|
||||
pendingCheckSec = 30 // pending 확인 주기(초)
|
||||
pendingCheckSec = 5 // pending 확인 주기(초) — 시장가 주문 즉시 체결 대응
|
||||
)
|
||||
|
||||
// AutoTradeService 자동매매 엔진 서비스
|
||||
@@ -37,14 +38,14 @@ type AutoTradeService struct {
|
||||
stockSvc *StockService
|
||||
themeSvc *ThemeService
|
||||
|
||||
mu sync.RWMutex
|
||||
running int32 // atomic: 1=실행, 0=중지
|
||||
rules []models.AutoTradeRule
|
||||
positions map[string]*models.AutoTradePosition // code → 포지션
|
||||
logs []models.AutoTradeLog // 최근 maxLogEntries건
|
||||
cooldown map[string]time.Time // code → 마지막 진입 시각
|
||||
watchSource models.AutoTradeWatchSource // 감시 소스 설정
|
||||
logBroadcaster func(models.AutoTradeLog) // WS 브로드캐스트 콜백
|
||||
mu sync.RWMutex
|
||||
running int32 // atomic: 1=실행, 0=중지
|
||||
rules []models.AutoTradeRule
|
||||
positions map[string]*models.AutoTradePosition // code → 포지션
|
||||
logs []models.AutoTradeLog // 최근 maxLogEntries건
|
||||
cooldown map[string]time.Time // code → 마지막 진입 시각
|
||||
watchSource models.AutoTradeWatchSource // 감시 소스 설정
|
||||
logBroadcaster func(models.AutoTradeLog) // WS 브로드캐스트 콜백
|
||||
}
|
||||
|
||||
var autoTradeService *AutoTradeService
|
||||
@@ -495,15 +496,20 @@ func (s *AutoTradeService) checkEntries() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 포지션 등록
|
||||
// 포지션 등록 (현재가 기준 예상 손절/익절가 미리 계산 → UI에 0원 방지)
|
||||
estStop := int64(float64(sig.CurrentPrice) * (1 + rule.StopLossPct/100))
|
||||
estProfit := int64(float64(sig.CurrentPrice) * (1 + rule.TakeProfitPct/100))
|
||||
pos := &models.AutoTradePosition{
|
||||
Code: code,
|
||||
Name: sig.Name,
|
||||
Qty: qty,
|
||||
OrderNo: result.OrderNo,
|
||||
EntryTime: time.Now(),
|
||||
RuleID: rule.ID,
|
||||
Status: "pending",
|
||||
Code: code,
|
||||
Name: sig.Name,
|
||||
Qty: qty,
|
||||
BuyPrice: sig.CurrentPrice,
|
||||
StopLoss: estStop,
|
||||
TakeProfit: estProfit,
|
||||
OrderNo: result.OrderNo,
|
||||
EntryTime: time.Now(),
|
||||
RuleID: rule.ID,
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -580,6 +586,7 @@ func (s *AutoTradeService) checkExits() {
|
||||
}
|
||||
|
||||
// checkPending pending 포지션 체결 확인
|
||||
// 미체결 주문 조회(ka10075)에서 OrderNo가 사라지면 체결된 것으로 간주
|
||||
func (s *AutoTradeService) checkPending() {
|
||||
s.mu.RLock()
|
||||
pending := make([]*models.AutoTradePosition, 0)
|
||||
@@ -595,49 +602,71 @@ func (s *AutoTradeService) checkPending() {
|
||||
return
|
||||
}
|
||||
|
||||
balance, err := s.accountSvc.GetBalance()
|
||||
// 미체결 주문 목록 조회 → 주문번호 집합 구성
|
||||
unfilled, err := s.accountSvc.GetPendingOrders()
|
||||
if err != nil {
|
||||
log.Printf("[자동매매] 잔고 조회 실패: %v", err)
|
||||
log.Printf("[자동매매] 미체결 조회 실패: %v", err)
|
||||
return
|
||||
}
|
||||
unfilledMap := make(map[string]struct{}, len(unfilled))
|
||||
for _, o := range unfilled {
|
||||
unfilledMap[strings.TrimSpace(o.OrdNo)] = struct{}{}
|
||||
}
|
||||
|
||||
for _, pos := range pending {
|
||||
for _, stock := range balance.Stocks {
|
||||
if stock.StkCd == pos.Code {
|
||||
buyPrice, _ := strconv.ParseInt(stock.PurPric, 10, 64)
|
||||
qty, _ := strconv.ParseInt(stock.RmndQty, 10, 64)
|
||||
if qty <= 0 {
|
||||
continue
|
||||
}
|
||||
orderNo := strings.TrimSpace(pos.OrderNo)
|
||||
if _, stillPending := unfilledMap[orderNo]; stillPending {
|
||||
// 아직 미체결 목록에 있음 → 대기 유지
|
||||
s.addLog("debug", pos.Code, fmt.Sprintf("체결 대기 중 (주문번호: %s)", orderNo))
|
||||
continue
|
||||
}
|
||||
|
||||
// 청산가 계산
|
||||
rule := s.findRule(pos.RuleID)
|
||||
var stopLoss, takeProfit int64
|
||||
if rule != nil && buyPrice > 0 {
|
||||
stopLoss = int64(float64(buyPrice) * (1 + rule.StopLossPct/100))
|
||||
takeProfit = int64(float64(buyPrice) * (1 + rule.TakeProfitPct/100))
|
||||
}
|
||||
// 미체결 목록에서 사라짐 → 체결(또는 취소) 완료
|
||||
// 잔고에서 실제 매입단가 조회 시도
|
||||
buyPrice := s.getBuyPriceFromBalance(pos.Code)
|
||||
if buyPrice == 0 {
|
||||
buyPrice = pos.BuyPrice // checkEntries에서 설정한 현재가 예상값
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if p, ok := s.positions[pos.Code]; ok && p.Status == "pending" {
|
||||
p.BuyPrice = buyPrice
|
||||
p.Qty = qty
|
||||
p.StopLoss = stopLoss
|
||||
p.TakeProfit = takeProfit
|
||||
p.Status = "open"
|
||||
rule := s.findRule(pos.RuleID)
|
||||
var stopLoss, takeProfit int64
|
||||
if rule != nil && buyPrice > 0 {
|
||||
stopLoss = int64(float64(buyPrice) * (1 + rule.StopLossPct/100))
|
||||
takeProfit = int64(float64(buyPrice) * (1 + rule.TakeProfitPct/100))
|
||||
}
|
||||
|
||||
// ExitBeforeClose 규칙 값 저장
|
||||
if rule != nil {
|
||||
// 포지션 자체에 규칙 정보가 없으므로 로그만 기록
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.mu.Lock()
|
||||
if p, ok := s.positions[pos.Code]; ok && p.Status == "pending" {
|
||||
p.BuyPrice = buyPrice
|
||||
p.StopLoss = stopLoss
|
||||
p.TakeProfit = takeProfit
|
||||
p.Status = "open"
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
s.addLog("info", pos.Code, fmt.Sprintf("매수 체결 확인: %s %d주 @ %d원 (손절: %d, 익절: %d)", pos.Name, qty, buyPrice, stopLoss, takeProfit))
|
||||
break
|
||||
}
|
||||
s.addLog("info", pos.Code, fmt.Sprintf("매수 체결 확인: %s %d주 @ %d원 (손절: %d, 익절: %d)",
|
||||
pos.Name, pos.Qty, buyPrice, stopLoss, takeProfit))
|
||||
}
|
||||
}
|
||||
|
||||
// getBuyPriceFromBalance 잔고 조회로 종목 매입단가 반환 (없으면 0)
|
||||
func (s *AutoTradeService) getBuyPriceFromBalance(code string) int64 {
|
||||
balance, err := s.accountSvc.GetBalance()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, stock := range balance.Stocks {
|
||||
stkCd := strings.TrimSpace(stock.StkCd)
|
||||
// 키움 API 코드 형식 A-prefix 처리 (예: "A005930" → "005930")
|
||||
if strings.HasPrefix(stkCd, "A") {
|
||||
stkCd = stkCd[1:]
|
||||
}
|
||||
if stkCd == code {
|
||||
price, _ := strconv.ParseInt(strings.TrimSpace(stock.PurPric), 10, 64)
|
||||
return price
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// executeSell 매도 주문 실행 및 포지션 업데이트
|
||||
|
||||
Reference in New Issue
Block a user