본문 바로가기
AyoProject/Ayotera-Trade

[AT] 30. 우량주 종목 자동 예측 및 선정 (4-4)

by 청양호박이 2021. 2. 23.

이제 어느정도 우량주 종목 중 저평가 종목에 대한 추출의 로직이 마무리 단계에 접어들고 있습니다. DB상에는 이제 원하는 모든데이터가 저장되어 있으며... 우량주 종목 중 저평가 종목에 대한 추출로직을 spring boot에 넣어서 화면에 렌더링을 해보겠습니다.

 

  • 우량주 종목 선정을 위한 기반 데이터 수집 방안 (전자공시시스템 활용) - 1개 종목기준
  • 정리된 데이터 수집 방안에 대해서 코스피, 코스닥 전체 종목에 대한 데이터를 DBMS에 적재
  • 우량주 종목 선정 로직을 적용하여 대상 종목 추출
  • 추출된 우량주 종목에 대해서 저평가 종목 추출
  • 추출된 종목에 대해서 실험실 진행

 

우량주 종목에 대해서 저평가 종목을 추출하는 내용을 완성하기 위해서는 아래의 추가 단계가 필요하고, 앞으로 당분간은 해당 구성을 구현해 보도록 하겠습니다. 

 

앞으로 구성해야할 전체 구성입니다. 현재 완료되어있는 부분과 안되어 있는 부분을 단계별로 확인해 보면....

 

  1. Dart OpenData로 부터 전체 종목의 재무제표정보 추출 (완료)
  2. 영업이익률 / 현금흐름표를 기준으로 우량주 종목을 선정 (완료)
  3. 선정된 우량주 종목을 Database에 저장 (완료)
  4. 키움증권 API로 부터 3번에 저장된 우량주 종목에 대한 지표를 추출 (완료)
  5. 추출된 종목별 지표를 Database에 저장 (완료)
  6. 통합 이력기록을 위해서 작업 이력을 이력테이블 생성 및 저장
  7. 저평가 종목을 포함한 최종 종목을 선정 및 화면에 렌더링

 

python을 통해서는 일배치로 동작하는 로직이 반영되어 있습니다. 이를 통해서 Dart(전자공시시스템)에서 제공하는 OPEN DATA를 통해서 재무제표 정보를 가져오고... 그 데이터를 통해서 1차 가공하여 영업이익률 + 현금흐름표에 대해서 조건에 부합하는 대상을 추출합니다. 마지막으로는 1차 가공을 통해서 구한 종목에 대해서 키움증권에서 제공하는 Open API를 통해서 종목기본데이터를 가져와서 저장하게 됩니다.

 

spring boot에서는 일배치로 반영된 데이터에 대해서 가져와서 2차로 가공하여 우량주 중 저평가 종목을 선별하여 화면에 보여주게 됩니다. 그럼 해당 부분에 대해서 찬찬히 살펴보겠습니다. 

 

 

1. DB Query 작성


모든 데이터는 준비가 되었기 때문에, 우리는 관련된 DB를 순차적으로 조합해서 결과를 가져오면 됩니다. 관련된 DB는 아래와 같습니다. 

 

  • quarter_superiror_stocks - 분기별 우량주 종목 list 저장 table
  • stocks_basic_info - 분기별 우량주 종목을 기준으로 키움증권 API를 통해서 가져온 기본 정보 저장 table
  • stocks_info - 코스피/코스닥 전체 종목에 대한 id, 종목명 매핑 list 저장 table

 

이렇게 3가지 테이블이 사용됩니다. 우량주 종목 list를 기준으로 필요한 table을 left join하여 필요한 부가정보를 붙여줍니다. 또한, 화면에서 년도별 / 분기별 / ROE / 시가총액에 따른 검색을 시도할 예정이기 때문에 조건절(where)을 추가하여 작성합니다.

SELECT 
	A.stocks_id,
	A.bsns_year,
	A.bsns_quarter,
	B.stocks_roe,
	B.stocks_pbr,
	B.stocks_marketcap,
	C.stocks_type,
	C.stocks_name
FROM quarter_superior_stocks AS A
LEFT JOIN stocks_basic_info AS B
ON A.stocks_id = B.stocks_id
LEFT JOIN stocks_info AS C
ON A.stocks_id = C.stocks_id
WHERE A.bsns_year = '2020' AND A.bsns_quarter = '1' AND B.stocks_roe > 3 AND B.stocks_marketcap > 5000
ORDER BY cast(B.stocks_pbr AS DECIMAL(5,2)) ASC;

우리가 PBR에 대해서는 우선 낮은 값을 더 저평가된 종목으로 판단하기로 했기 때문에... stocks_pbr을 기준으로 오름차순 정렬하여 구현합니다.

 

[주의!!]

 

현재 개발되는 모든 table은 컬럼(column)이 varchar로 되어있습니다. 따라서 숫자의 크기에 따라서 정렬이 필요할 경우에는 형변환(cast)이 필요합니다. 이렇게 된 원인에는 DART에서 제공하는 OPEN DATA도 그렇고... 키움증권의 Open API도 모두 결과를 string으로 리턴해 주는 부분도 반영된 결과입니다.

 

따라서, mysql에서 string을 double로 형변환(cast)하여 정렬을 위해서는 추가적인 방법이 필요한데...

 

cast(컬럼명 AS DECIMAL(소수점앞자리수, 소수점뒷자리수)

 

와 같이 구현해서 사용하면 됩니다. 자 그렇다면 Query의 결과는?? 아래와 같이 잘 나타나게 됩니다. 

2020년 1분기의 결과로 2분기에 우량주 중 저평가된 항목으로 투자를 고민할 항목들입니다. 총 42개의 종목이 추출되었으며, 그 중에서 상위 top10에 해당 하는 종목이 보여지네요...

 

 

2. AT_Back(Spring Boot) 프로그램 작성


이제 그냥 python작성 부분, Spring Boot 작성부분, vue.js 작성부분... 이렇게 부르지 않고, 뭔가 모듈로 구분해서 이름을 지어보겠습니다. 

 

  • AT_Front - vue.js로 작성되는 화면에 보여지는 역할을 담당하는 부분
  • AT_Back (Spring Boot) - Database <-> AT_Front 간 중계기능을 하며 일부 로직이 들어가는 부분
  • AT_Back (Python) - 각종 타 시스템이 제공하는 API를 통해서 데이터를 가져오는 부분

 

자 그렇다면, 위에서 알아본 Query를 녹여서 구현할 AT_Back(Spring Boot)을 구현해 보겠습니다. AT_Back에 작성되야 하는 파일은 총 6개 파일입니다. 그 중에서는 기존에 작성되어있는 Controller나 Service 그리고 Mapper에 작성이 가능하겠지만 우량주라는 별도의 기능을 가지는 페이지이기 때문에 완벽하게 분리하여 새로 만들겠습니다.

 

  • SearchDTO - vue.js의 axios.get에 추가되는 params를 넘겨받는 DTO
  • Controller - 웹브라우저를 통해서 요청(request)하는 path를 정의하는 부분
  • Service - Transaction단위의 작업을 그룹화하고 Mapper에게 작업을 요청하는 부분
  • Mapper - Instance로 실제 쿼리가 들어가는 Mapper.xml 와 연계하는 부분
  • Mapper.xml - 실제 쿼리로 Database에 직접 접근하여 결과를 가져와서 QSS List DTO 객체에 넣어 전달
  • QSS List DTO - Database로부터 가져온 데이터를 저장하는 DTO

 

이렇게 6개의 파일을 작성하면 구현이 가능합니다.

 

[SearchDTO]

public class QSSListSearch {
	
	private String bsnsYear;
	private String bsnsQuarter;
	private Double stocksRoe;
	private Integer stocksMarketcap;

}

코드가 길어져서 getter( )와 setter( )는 생성해야 합니다.

 

[Controller]

@RestController
@RequestMapping(value = "/qss")
public class QSSController {
	
	@Autowired
	QSSService qssService;
	
	@RequestMapping(value = "list", method = RequestMethod.GET)
	public ResponseEntity<ArrayList<QSSListDTO>> listQSS(QSSListSearch condition){
		return ResponseEntity.ok(qssService.listQSS(condition));
	}

}

RestController로 정의하며, 해당 API에 접근하기 위해서는 /qss/list로 접근하여 결과를 가져올 수 있습니다. 이 주소를 vue.js에서 axios.get의 url로 사용하게 됩니다.

 

[Service]

@Service
@Transactional
public class QSSService {
	
	@Autowired
	QSSMapper qssMapper;
	
	public ArrayList<QSSListDTO> listQSS(QSSListSearch condition) {
		return qssMapper.listQSS(condition);
	}

}

 

[Mapper]

@Mapper
public interface QSSMapper {

	ArrayList<QSSListDTO> listQSS(@Param("search") QSSListSearch condition);

}

 

[Mapper.xml]

<select id="listQSS" resultType="com.ayoteralab.atproject.main.dto.QSSListDTO">
<![CDATA[
	SELECT 
		A.stocks_id,
		A.bsns_year,
		A.bsns_quarter,
		B.stocks_roe,
		B.stocks_pbr,
		B.stocks_marketcap,
		C.stocks_type,
		C.stocks_name
	FROM quarter_superior_stocks AS A
	LEFT JOIN stocks_basic_info AS B
	ON A.stocks_id = B.stocks_id
	LEFT JOIN stocks_info AS C
	ON A.stocks_id = C.stocks_id
	WHERE 1=1
]]>
<if test=" search.bsnsYear != null and search.bsnsYear != '' ">
		AND A.bsns_year = #{search.bsnsYear}</if>
<if test=" search.bsnsQuarter != null and search.bsnsQuarter != '' ">
		AND A.bsns_quarter = #{search.bsnsQuarter}</if>
<if test=" search.stocksRoe != null ">
		AND B.stocks_roe > #{search.stocksRoe}</if>
<if test=" search.stocksMarketcap != null ">
		AND B.stocks_marketcap > #{search.stocksMarketcap}</if>
	ORDER BY CAST(B.stocks_pbr AS DECIMAL(5,2)) ASC;
</select>

 

[QSS List DTO]

public class QSSListDTO {
	
	private String stocksId;
	private String bsnsYear;
	private String bsnsQuarter;
	private String stocksRoe;
	private String stocksPbr;
	private String stocksMarketcap;
	private String stocksType;
	private String stocksName;
    
}

역시나 코드가 길어져서 getter( )와 setter( )는 생성해야 합니다. 추가적으로 해당 명으로 vue.js에 response로 들어가게 됩니다.

 



 

 

3. AT_Front 프로그램 작성


우선 최종적인 모습을 살펴보겠습니다.

vue.js에서 크게는 윗부분에 v-form이 들어가고, 아래부분에는 v-data-table 과 v-pagination이 들어갑니다.

 

  • v-form - 검색조건을 넣는 v-text-field를 잘 정렬되게 구성
  • v-data-table - axios.get으로 받은 response의 data를 grid형식으로 표시
  • v-pagination - 다량의 결과가 도출된 경우 page를 나누기 위한 부분
  • methods - axios.get을 통해서 API를 호출하는 로직 구현

 

그럼 부분별로 알아보도록 하겠습니다. 세부적인 tag의 props까지는 기술하지 않고 해당 화면을 렌더링하기 위해서 사용된 실질적인 code만 확인하겠습니다.

(개인적인 style로 tunning을 하기 위해서는 vuetify의 document를 확인하시면 되겠습니다.)

 

[v-form]

[template]
      <v-form v-model="valid">
        <v-container>
          <v-row>
            <v-col cols="3">
              <v-text-field v-model="searchCondition.targetYear" :rules="yearRules"
                :counter="4" label="Target Year" required></v-text-field>
            </v-col>
            <v-col cols="3">
              <v-text-field v-model="searchCondition.targetQuarter" :rules="quarterRules"
                :counter="1" label="Target Quarter" required></v-text-field>
            </v-col>
            <v-col cols="3">
              <v-text-field v-model="searchCondition.overROE"
                :counter="10" label="Over ROE" required></v-text-field>
            </v-col>
            <v-col cols="3">
              <v-text-field v-model="searchCondition.overMarketCap"
                :counter="10" label="Over MarketCap" required></v-text-field>
            </v-col>
          </v-row>
          <v-btn class="ma-2" outlined color="indigo" @click="getQSSList" block>Search</v-btn>
        </v-container>
      </v-form>
      
[script]
  data() {
    return {
      searchCondition: {
        targetYear: '',
        targetQuarter: '',
        overROE: null,
        overMarketCap: null,
      },
      yearRules: [
        v => !!v || 'Year is required',
        v => v.length === 4 || 'Year must be equal 4 number',
        v => /^[0-9]+$/.test(v) || 'Only number possible',
      ],
      quarterRules: [
        v => !!v || 'Quarter is required',
        v => v.length === 1 || 'Quarter must be equal 1 number',
        v => /^[1-4]$/.test(v) || 'Only 1~4 number possible',
      ],
    };
  },

 

[v-data-table]

[template]
        <v-data-table
          :headers="headers"
          :items="items"
          :loading="loading"
          loading-text="Loading... Please wait"
          :page.sync="page"
          :items-per-page="itemsPerPage"
          hide-default-footer
          class="elevation-1"
          @page-count="pageCount = $event"></v-data-table>
          
[script]
  data() {
    return {
      headers: [
        { text: '년도', align: 'center', sortable: false, value: 'bsnsYear' },
        { text: '분기', align: 'center', sortable: false, value: 'bsnsQuarter' },
        { text: '종목명', align: 'center', sortable: false, value: 'stocksName' },
        { text: '종목코드', align: 'center', sortable: false, value: 'stocksId' },
        { text: '구분', align: 'center', sortable: false, value: 'stocksType' },
        { text: 'ROE', align: 'center', sortable: false, value: 'stocksRoe' },
        { text: 'PBR', align: 'center', sortable: false, value: 'stocksPbr' },
        { text: '시가총액', align: 'center', sortable: false, value: 'stocksMarketcap' },
      ],
      items: [],
      loading: true,
      page: 1,
      itemsPerPage: 10,
    };
  },

 

[v-pagination]

[template]
          <v-pagination
            v-model="page"
            :length="pageCount"
            :total-visible="totalVisible"
            next-icon="mdi-menu-right"
            prev-icon="mdi-menu-left"></v-pagination>
            
[script]
  data() {
    return {
      page: 1,
      itemsPerPage: 10,
      pageCount: 0,
      totalVisible: 10,
    };
  },

 

[methods]

  created() {
    this.getQSSList();
  },
  methods: {
    getQSSList() {
      console.log(this.searchCondition.overROE);
      const url = '/qss/list';
      axios.get(url, {
        params: {
          bsnsYear: this.searchCondition.targetYear,
          bsnsQuarter: this.searchCondition.targetQuarter,
          stocksRoe: this.searchCondition.overROE,
          stocksMarketcap: this.searchCondition.overMarketCap,
        },
      }).then((res) => {
        this.items = res.data;
        this.loading = false;
      }).catch((error) => {
        console.log(error);
      }).then(() => {
        console.log('getQSSList End!!');
      });
    },
  },

사실 methods는 아니지만 created( )단계가 들어왔을때, axios.get이 있는 methods를 호출함으로써 초기에 데이터를 가져와서 보여줄 수 있습니다.

 

그리고 getQSSList( )는 /qss/list 를 통해서 원하는 params를 입력하고 전송하게 됩니다. 뒷단에는... 우리가 바로위에 작성했던 코드들이 유기적으로 연동되어 response를 돌려주게 됩니다. 그리고 res.data를 통해서 값을 렌더링에 사용하게 되는 것 입니다.

 

- Ayotera Lab -

댓글