키움증권 REST API 실시간시세조회 및 조건검색 > PHP
PHP

키움증권 REST API 실시간시세조회 및 조건검색

조회 5,167회 댓글 0건
  • 현재 페이지 주소 복사
  • 페이스북으로 공유
  • X 로  공유
  • 트위터로  공유
  • 네이버 블로그로 공유
  • 네이버 카페 공유하기
  • 네이버 라인 공유하기
  • 네이버 밴드 공유하기
  • 링크드인으로 공유하기
  • 핀터레스트에 공유하기

이 프로그램 이용하기 전에 인증 토큰을 먼저 받아야 하는데 아래 프로그램 참고 하면 됩니다.

키움증권 REST API 접근토큰 발급


실시간시세조회가 필요한 이유는 별도 요청 없이 한번 등록하면 자동으로 여러개의 종목코드에 대한 시세 정보를 얻을 수 있습니다.

그리고 조검검색에서 실시간 실행되는 기능 때문인지 조건검색도 웹소켓을 이용하게 되어 있습니다.


 



아래는 테스트까지 해본것으로 잘되는 프로그램으로 테스트 후 자신에 맞게 수정해 사용하면 됩니다.



$aAuth2Token = fn_au10001($aParams);
$accessToken = $aAuth2Token['token'];
echo $accessToken . PHP_EOL;
sleep(1);


class WebSocketClient {
    private $uri;
    private $socket;
    private $connected;
    private $keepRunning;
    private $accessToken;
    private $host;
    private $port;
    private $path;

    public function __construct($uri, $accessToken) {
        $this->uri = $uri;
        $this->accessToken = $accessToken;
        $this->connected = false;
        $this->keepRunning = true;
        $this->parseUri($uri);
    }

    private function parseUri($uri) {
        $parsed = parse_url($uri);
        $this->host = $parsed['host'];
        $this->port = $parsed['port'] ?? ($parsed['scheme'] === 'wss' ? 443 : 80);
        $this->path = $parsed['path'] ?? '/';
        if (isset($parsed['query'])) {
            $this->path .= '?' . $parsed['query'];
        }
    }

    public function connect() {
        echo "서버와 연결을 시도 중입니다.\n";

        // SSL 컨텍스트 생성 (wss:// 연결용)
        $context = stream_context_create([
            'ssl' => [
                'verify_peer' => false,
                'verify_peer_name' => false,
                'allow_self_signed' => true
            ]
        ]);

        // SSL 소켓 연결
        $this->socket = stream_socket_client(
            "ssl://{$this->host}:{$this->port}",
            $errno,
            $errstr,
            30,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if (!$this->socket) {
            echo "Connection error: $errstr ($errno)\n";
            return false;
        }

        // 웹소켓 핸드셰이크 수행
        if (!$this->performHandshake()) {
            echo "핸드셰이크 실패\n";
            return false;
        }

        $this->connected = true;
        echo "웹소켓 서버에 연결되었습니다.\n";

        // 로그인 패킷 전송
        $loginParam = [
            'trnm' => 'LOGIN',
            'token' => $this->accessToken
        ];

        echo "실시간 시세 서버로 로그인 패킷을 전송합니다.\n";
        $this->sendMessage($loginParam);

        return true;
    }

    private function performHandshake() {
        // 웹소켓 핸드셰이크 키 생성
        $key = base64_encode(random_bytes(16));

        // HTTP 헤더 구성
        $headers = [
            "GET {$this->path} HTTP/1.1",
            "Host: {$this->host}:{$this->port}",
            "Upgrade: websocket",
            "Connection: Upgrade",
            "Sec-WebSocket-Key: $key",
            "Sec-WebSocket-Version: 13",
            "Origin: https://{$this->host}",
            "\r\n"
        ];

        $request = implode("\r\n", $headers);
        fwrite($this->socket, $request);

        // 응답 읽기
        $response = fread($this->socket, 1024);

        // 핸드셰이크 응답 검증
        if (strpos($response, '101 Switching Protocols') === false) {
            echo "핸드셰이크 응답이 올바르지 않습니다.\n";
            echo "응답: $response\n";
            return false;
        }

        return true;
    }

    public function sendMessage($message) {
        if (!$this->connected || !$this->socket) {
            echo "연결이 끊어져 있습니다.\n";
            return false;
        }

        // 배열이면 JSON으로 변환
        if (is_array($message)) {
            $message = json_encode($message, JSON_UNESCAPED_UNICODE);
        }

        // 웹소켓 프레임 생성
        $frame = $this->createFrame($message);

        $result = fwrite($this->socket, $frame);
        if ($result === false) {
            echo "메시지 전송 실패\n";
            return false;
        }

        echo "Message sent: $message\n";
        return true;
    }

    private function createFrame($message) {
        $length = strlen($message);
        $frame = '';

        // FIN bit (1) + RSV (000) + Opcode (0001 for text)
        $frame .= chr(0x81);

        // Mask bit (1) + Payload length
        if ($length < 126) {
            $frame .= chr($length | 0x80);
        } elseif ($length < 65536) {
            $frame .= chr(126 | 0x80);
            $frame .= pack('n', $length);
        } else {
            $frame .= chr(127 | 0x80);
            $frame .= pack('NN', 0, $length);
        }

        // Masking key (4 bytes)
        $mask = pack('N', mt_rand());
        $frame .= $mask;

        // Masked payload
        for ($i = 0; $i < $length; $i++) {
            $frame .= $message[$i] ^ $mask[$i % 4];
        }

        return $frame;
    }

    public function receiveMessage() {
        if (!$this->socket || !$this->connected) {
            return false;
        }

        // 첫 2바이트 읽기 (헤더)
        $header = fread($this->socket, 2);
        if (strlen($header) < 2) {
            return false;
        }

        $firstByte = ord($header[0]);
        $secondByte = ord($header[1]);

        // FIN bit 확인
        $fin = ($firstByte & 0x80) === 0x80;

        // Opcode 확인
        $opcode = $firstByte & 0x0F;

        // Close frame인 경우
        if ($opcode === 0x08) {
            echo "서버가 연결을 종료했습니다.\n";
            $this->connected = false;
            return false;
        }

        // Payload length 읽기
        $payloadLength = $secondByte & 0x7F;

        if ($payloadLength === 126) {
            $lengthData = fread($this->socket, 2);
            $payloadLength = unpack('n', $lengthData)[1];
        } elseif ($payloadLength === 127) {
            $lengthData = fread($this->socket, 8);
            $payloadLength = unpack('J', $lengthData)[1];
        }

        // Payload 읽기
        $payload = '';
        $bytesRead = 0;
        while ($bytesRead < $payloadLength) {
            $chunk = fread($this->socket, $payloadLength - $bytesRead);
            if ($chunk === false || strlen($chunk) === 0) {
                break;
            }
            $payload .= $chunk;
            $bytesRead += strlen($chunk);
        }

        return $payload;
    }

    public function handleMessage($message) {
        $response = json_decode($message, true);

        if ($response === null) {
            echo "JSON 파싱 실패: $message\n";
            return;
        }

        // 메시지 유형이 LOGIN일 경우 로그인 시도 결과 체크
        if (isset($response['trnm']) && $response['trnm'] === 'LOGIN') {
            if ($response['return_code'] != 0) {
                echo "로그인 실패하였습니다: " . $response['return_msg'] . "\n";
                $this->disconnect();
            } else {
                echo "로그인 성공하였습니다.\n";
                sleep(1);

                # 조건검색 목록조회
                $this->ka10171();


                // 로그인 성공 후 실시간 항목 등록 - 테스트는 장이 열렸을때 해야 한다.
                // $this->registerRealtime();
            }
        }
        // 메시지 유형이 PING일 경우 수신값 그대로 송신
        elseif (isset($response['trnm']) && $response['trnm'] === 'PING') {
            $this->sendMessage($response);
        }

        if (!isset($response['trnm']) || $response['trnm'] !== 'PING') {
            echo "서버 응답 수신: " . json_encode($response, JSON_UNESCAPED_UNICODE) . "\n";
        }

        // 예시: 특정 조건에서 자동 종료
        // if (isset($response['some_condition'])) {
        //     echo "조건이 충족되어 연결을 종료합니다.\n";
        //     $this->disconnect();
        // }
    }

    // 조건검색 목록조회
    public function ka10171() {
        $realtimeParam = [
            'trnm' => 'CNSRLST',      // CNSRLST 고정값
        ];

        echo "조건검색 목록조회 요청합니다.\n";
        $this->sendMessage($realtimeParam);
    }

    public function registerRealtime() {
        // 실시간 항목 등록
        $realtimeParam = [
            'trnm' => 'REG',      // REG : 등록 , REMOVE : 해지
            'grp_no' => '1',      // 그룹번호
            'refresh' => '1',     // 기존등록유지여부
            'data' => [[           // 실시간 등록 리스트
                'item' => ['039490','026040','337930'], // 실시간 등록 요소
                'type' => ['0B']      // 00:주문체결, OB: 주식체결
            ]]
        ];

        echo "실시간 시세 등록을 요청합니다.\n";
        $this->sendMessage($realtimeParam);
    }

    public function run() {
        if (!$this->connect()) {
            return;
        }

        // 논블로킹 모드로 설정
        stream_set_blocking($this->socket, false);

        $startTime = time();
        $maxRunTime = 3600; // 1시간 후 자동 종료 (초 단위)

        while ($this->keepRunning && $this->connected) {
            // 시간 제한 체크
            if (time() - $startTime > $maxRunTime) {
                echo "시간 제한에 도달하여 연결을 종료합니다.\n";
                break;
            }

            // 읽기 가능한 소켓 확인
            $read   = [$this->socket];
            $write  = null;
            $except = null;

            $result = stream_select($read, $write, $except, 1);

            if ($result === false) {
                echo "stream_select 에러\n";
                break;
            }

            if ($result > 0 && in_array($this->socket, $read)) {
                $message = $this->receiveMessage();
                if ($message !== false && $message !== '') {
                    $this->handleMessage($message);
                }
            }

            // CPU 사용률 줄이기 위한 작은 대기
            usleep(12300); // 12ms
        }

        $this->disconnect();
    }

    public function stop() {
        echo "수동으로 웹소켓 연결을 중지합니다.\n";
        $this->keepRunning = false;
    }

    public function disconnect() {
        $this->keepRunning = false;
        if ($this->connected && $this->socket) {
            // Close frame 전송
            $closeFrame = chr(0x88) . chr(0x80) . pack('N', mt_rand());
            @fwrite($this->socket, $closeFrame);

            fclose($this->socket);
            $this->connected = false;
            echo "Disconnected from WebSocket server\n";
        }
    }
}



// 시그널 핸들러 설정 (Ctrl+C 처리)
function signalHandler($signal) {
    global $websocketClient;
    echo "\n프로그램을 종료합니다.\n";
    if (isset($websocketClient)) {
        $websocketClient->disconnect();
    }
    exit(0);
}



// 실행 부분
function main($accessToken) {
    global $websocketClient;

    // 웹소켓 서버 정보 설정
    $socketUrl = 'wss://api.kiwoom.com:10000/api/dostk/websocket';  // 접속할 주소


    // WebSocketClient 인스턴스 생성
    $websocketClient = new WebSocketClient($socketUrl, $accessToken);

    echo "웹소켓 클라이언트를 시작합니다.\n";
    $websocketClient->run();

}

// 프로그램 실행
main($accessToken);

 


종목코드 넣을때 KRX는 아무것도 없이 코드만

NXT 의 경우 123456_NX 이렇게 넣는것입니다.

저처럼 예제에 NXT:123456_NX 되어 있다고 앞쪽에 있는 NXT: 까지 넣으면 아무것도 나오질 않습니다.


● 조건검색 일반 및 실시간

조건검색 요청 일반   ka10172

조건검색 요청 실시간 ka10173


메뉴얼을 보는 방법이 제가 잘못 되었거나 부실하거나 둘중 하나인 부분이 있는데 

아무리 해도 안되길레 소스를 봤더니 조건검색 목록조회(ka10171) 를 먼저 한번 호출한 다음 ka10172 호출을 해야 합니다.

소스를 보면 대략 다음과 같습니다.


        // 메시지 유형이 LOGIN일 경우 로그인 시도 결과 체크
        if (isset($response['trnm']) && $response['trnm'] === 'LOGIN') {
            if ($response['return_code'] != 0) {
                echo "로그인 실패하였습니다: " . $response['return_msg'] . "\n";
                $this->disconnect();
            } else {
                echo "로그인 성공하였습니다.\n";
                sleep(1);

                # 조건검색 목록조회
                if ( $this->runType == 'ka10171' ) {    // 조건검색 목록조회
                    $this->ka10171();
                }
                elseif ( $this->runType == '조건검색_일반' ) {
                    $this->ka10171();
                    $this->ka10172(91);
                }
                elseif ( $this->runType == '조건검색_실시간' ) {
                    $this->ka10171();
                    $this->ka10173(91);
                }
                elseif ( $this->runType == '실시간시세' ) { // 실시간시세
                    $this->registerRealtime();
                }
            }
        }
 


**중요**

실시간 처리는 전달되는 방식이 비동기 입니다. 즉, 언제 결과가 올지 모릅니다.

위에 제가 한방식은 조건검색을 먼저 실행을 해야 그 다음것이 실행되기 때문에 위처럼 한것으로 

조건검색 결과를 받은 다음 번호를 얻어서 주는 방식은 위처럼 하면 안됩니다.


- 요즘은 프로그램 알려 달라고 하거나 동기식 처럼 처리 하고 싶다고 하면 프로그램 다시 작성해 주는것을 이용 하시면 됩니다.



● 참고 하면 좋은것

  - 키움증권 REST API 시세조회 > 시분요청 


  • 현재 페이지 주소 복사
  • 페이스북으로 공유
  • X 로  공유
  • 트위터로  공유
  • 네이버 블로그로 공유
  • 네이버 카페 공유하기
  • 네이버 라인 공유하기
  • 네이버 밴드 공유하기
  • 링크드인으로 공유하기
  • 핀터레스트에 공유하기
전체 237건 1 페이지
  • profile_image PHP 설치 되어 있지 아니하여 윈도우PC에 최신버전으로 설치버전은 설치 할 때마다 다르기 때문에 본인이 사용하려는버전을 선택 하면 됩니다.     > 현대적인? 방법으로 설치함.       예전에 압축해 놓은것 풀고 path 설정하고 했던 그런 방법을 쓰지 않고 간단하게 설치가 되었다.      winget install PHP.PHP.8.5    > 아래 명령으로 설치된 위치를 찾음      where php    > php.ini-production…
  • profile_image OP캐쉬 사용하면 괜찮아 보다는 저 같은 경우는 사용하지 않아도 괜찮아를 더 좋아 합니다.▷ 솔리드 캐시(SOLID CACHE)란?솔리드 캐시는 간단히 말해 "비싼 RAM(REDIS) 대신 저렴하고 넉넉한 디스크(DB)에 캐시를 저장하는 전략"으로 원래 루비 온 레일즈(RUBY ON RAILS) 커뮤니티에서 제안된 방식이지만 본질은 어떤 언어에서든 적용 가능한 실용적인 캐싱 철학임.▷ 핵심 철학: "ssd는 생각보다 훨씬 빠르다"과거에는 디스크가 너무 느려서 무조건 데이터를 ram(REDIS MEMCACHED)에 올려야 했지만 지금은 nvme ssd 같은 초고속 저장 장치가 보편화되었습니다. 굳이 복잡하게 별도의 메모리…
  • profile_image MyISAM은 SELECT가 빠르고 InnoDB는 느리다그런 경우도 있고 아닌 경우도 있기 때문에 어떤 용도로 사용하느냐에 따라서 다를 수 있습니다.그리고 처음 데이터 넣은 다음 select만 90% 이상이고 테이블 사용이 업데이트나 인서트는 적은 경우인지 불특정 다수에게 서비스 하기 때문에 불특정한 row를 가져와서 보여줘야 하는것인지에 다를 수 있는 것입니다. 가장 큰 차이: 데이터와 인덱스 구조→ MyISAM  - 데이터 파일(.MYD) 과 인덱스 파일(.MYI) 이 분리됨  - 인덱스 → 데이터 파일을 다시 읽는 구조  - 동작흐름: PK 인덱스 탐색 (.MYI) -> 데이터 위치…
  • profile_image 데이터베이스를 사용하다 보면 이미 존재하는 데이터인지 확인한 후 INSERT 또는 UPDATE를 해야 하는 상황을 자주 만나게 됩니다.이때 매우 유용한 문법이 바로 INSERT ... ON DUPLICATE KEY UPDATE입니다.즉, 쿼리 한번으로 해결 된다는 의미 인데 아무곳에서나 사용 가능한것은 아니고 키 중복이 발생하는 부분에서만 사용 하는 것입니다.그렇기 때문에 unique의 특성을 모르시는 분은 사용 하면 안되겠지요.  장점- 쿼리 수 감소: SELECT → INSERT/UPDATE 두 번 쿼리 날릴 필요 없음- 동시성 문제 감소: SELECT 후 INSERT 방식보다 Race Condition 발…
  • profile_image 웹서버에 접근하는 X-Forwarded-For란?X-Forwarded-For(XFF)는 HTTP 헤더로 클라이언트가 프록시나 로드 밸런서를 거쳐 웹 서버에 접속할 때 원래 클라이언트의 IP 주소를 식별하기 위해 사용 됩니다.로그밸런스도 프록시서버의 변형된 형태가 있기 때문에 쉽게 프록시 서버라고 생각을 하면 쉽습니다.일반적으로 웹 서버는 직접 연결된 IP만 볼 수 있는데 프록시 뒤에 있으면 프록시 서버의 IP만 보이게 되는데 이런 경우 XFF 헤더가 실제 사용자 IP를 전달하는 역할을 합니다. ▷ 헤더 형식X-Forwarded-For: 클라이언트IP 프록시1 프록시2 ...여러 프록시를 거치면 쉼표로 구분되어 …
  • profile_image ifconfig는 초기 한번 정도만 사용하고 interface configuration의 약자로 리눅스/유닉스 시스템에서 네트워크 인터페이스를 설정하고 확인하는 명령어입니다.▷ ifconfig 주요 용도1. 네트워크 인터페이스 정보 조회  - 현재 활성화된 네트워크 인터페이스 목록 확인  - 각 인터페이스의 IP 주소, MAC 주소, 서브넷 마스크 확인  - 네트워크 통계 정보 (전송/수신 패킷 수, 에러 등) 확인2. 네트워크 인터페이스 설정  - IP 주소 할당 및 변경  - 서브넷 마스크 설정  - 브로드캐스트 주소 설정  - MTU(Maximum Tr…
  • profile_image 결론부터 말하면 느리게 다운로드 처리 하는 방식에 대한 것입니다.왜? 느리게 다운로드가 필요한가 하면 지연을 시키면 트래픽이 평탄화 되는 효과가 있는데 갑자기 순간적으로 팍 튀는 그런 현상을 없애기 위해서 필욯나 방법중 하나 입니다. 원리는 간단하며 읽은 만큼 내보내고 약간 지연 시키는 방식 입니다.    /**     * 청크 단위로 지연시키면서 파일 다운로드 (대용량 파일용)     *     * @param string $filePath 다운로드할 파일 경로     * @param …

상업적 이용 금지. 컨텐츠는 개인 용도로만 사용이 가능 합니다.