반응형

 

 

Spring Security 활용 JWT 구현하기

 

왜 JWT를 선택 하였는가?

쿠키


  • 클라이언트에 300개 까지 저장 가능, 도메인당 20개의 값 가질 수 있다.
  • 쿠키는 사용자가 요청하지 않아도, 브라우저가 Request 시에 Request Header를 넣어 자동으로 서버에 전송한다.
  • 기존에는 쿠키에 사용자의 인증 정보를 담아서 보내는 방식 사용
    • 문제점 : 사용자의 인증 정보가 탈취될 가능성이 있다.

세션


  • 서버에서 클라이언트를 구분하기 위해 세션 ID를 부여한다.
  • 해당 세션을 서버측에서 관리하며, 사용자는 쿠키에 세션정보를 담아 보내면 서버에서 유효한 사용자인지 판단한다.
  • 장점
    • 데이터가 서버에 있기 때문에, 세션이 탈취당해도 사용자의 인증정보는 안전할 수 있다.
  • 단점
    • 사용자가 많아질수록 서버 메모리를 많이 차지하게 된다.
    • 서버가 여러개인 환경에서 세션을 동기화 시킬 때, 추가적인 시간이 소요된다.
  • 해결방안
    • 레디스를 활용하여 하나의 서버에서 세션을 관리한다.
    • 로드밸런싱을 통해 특정 IP에서 들어오는 요청은, 특정 서버로만 갈 수 있도록 한다.

JWT


  • 구성
    • Header, Payload, Signature
  • 생성 과정
    1. Header에 Signature 해싱에 사용할 알고리즘 방식을 지정하여 토큰 검증에 사용
    1. Payload에 사용자의 정보 및 토큰 만료 기한 적음
    1. Header와 Payload를 각각 BASE64로 인코딩
    1. Secret Key로 헤더에서 정의한 알고리즘을 통해 해싱 한다
    1. 위 값을 다시 BASE64로 인코딩 하여 생성한다
  • 문제점
    • 토큰이 탈취 당하면, 만료기한 전까지 탈취당한 토큰이 악용당할 수 있다.
    • 토큰 탈취 시나리오를 생각하면, 토큰의 유효시간을 짧게 설정 해야 한다.
    • 토큰의 유효시간이 짧을 경우 사용자는 계속 재로그인을 해야하는 불편함이 생긴다.
  • 해결방안
    • Access Token + Refresh Token 같이 사용
    • 인가를 담당하는 Access Token은 기간을 짧게 설정하고
    • 재발급을 담당하는 Refresh Token은 기간을 길게 설정할 수 있다.
  • 토큰 보관 방법
    • Access Token
      • 단독 사용 : 브라우저의 세션 스토리지에 저장
      • 세션 스토리지의 문제점 - JS로 토큰 값을 꺼내서 보내는 방식이기 때문에, XSS에 취약
      • Refresh Token과 함께 사용
        • 쿠키에 담을 때 : Refresh Token도 쿠키에 담기 때문에, Refresh Token과 함께 같이 탈취당할 수 있다.
        • 대안 : 자바 스크립트의 private 변수로 저장해 둔다.
          • 예상 문제점 - 페이지 이동시 마다 재발급 필요하지 않는가?
          • 해결방안 1. Redis를 활용하여 토큰의 발급 속도를 향상 시킨다.
          • 해결방안 2. React, Vue 처럼 SPA를 활용하여 페이지 이동 없이 토큰 만료시에만 재발급 할 수 있다.
    • Refresh Token
      • 쿠키에 보관 : 쿠키도 JS로 접근 가능하기 때문에, HTTP Only 옵션과 HTTPS가 적용되지 않은 이미지 등으로 탈취 되는것을 방지하기 위해 Secure 옵션을 설정해준다.
      💡
      Refresh Token을 발급만 해주면 보안상으로 문제가 없는 것인가?
      • 그렇지 않다!!!!
      • 토큰 탈취 시나리오
        1. 쿠키에 담긴 Refresh Token 탈취
        1. 기존 사용자는 Access Token 활용 및 추후 만료시 Refresh Token으로 재발급 요청
        1. 해커는 탈취한 Refresh Token으로 Access Token 재발급
        1. Refresh Token은 만료 기한이 길기 때문에, 만료기간 도달까지 해커는 Refresh Token으로 Access Token 재발급 받아 사용할 수 있음.
        1. 하나의 Refresh Token으로 두명이 사용하는 문제가 생김
      • 대안
        • Refresh Token Rotation 도입
          • 새로운 Access token 발급 요청이 들어올 때 마다, 리프레시 토큰을 교체하는 방식
          • 이 경우 Refresh Token은 하나에 한개의 토큰만 발급할 수 있게 된다.
          • 토큰 탈취 시나리오
            1. 쿠키에 담긴 Refresh Token 탈취
            1. Refresh Token 사용해서 재발급 요청 - 사용된 Refresh Token을 DB에 저장
              • 사용자가 요청할 경우 : Access + Refresh Token을 사용자에게 재발급
              • 해커가 요청할 경우 : Access + Refresh Token을 해커에게 재발급
            1. 추후 사용자 또는, 해커가 이미 사용된 Refresh Token으로 재발급 요청
            1. 재발급 요청이 들어올 경우 해당 사용자와 관련된 모든 Refresh Token을 Invalid 상태로 만듬.
            1. 이후 해커의 Refresh Token은 사용 불가능 상태가 되고, 사용자는 재로그인을 통해 Refresh Token과 Access Token을 다시 발급 받을 수 있음.
          •  

Spring Security를 활용한 JWT 구현

 

TokenInfo


@Builder
@Data
@AllArgsConstructor
public class TokenInfo {
    private String grantType;
    private String accessToken;
    private String refreshToken;
}
  • Builder 패턴을 적용하였다.
  • @Data는 지양하는 것이 좋지만, 개발 편의성을 위해 우선 사용하였다.

JwtTokenProvider


@Slf4j
@Component
public class JwtTokenProvider {


    private Key secret;
    @Value("${jwt.access-expired}")
    private Long accessTokenExpired;
    @Value("${jwt.refresh-expired}")
    private Long refreshTokenExpired;

    public JwtTokenProvider(@Value("${jwt.secret}") String secret) {
        byte[] keyBytes = secret.getBytes();
        this.secret = Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 유저 정보를 통해 토큰 생성
     */
    public TokenInfo generateToken(Authentication authentication) {
        log.info("generateToken start");

        long now = (new Date()).getTime();
        Date accessTokenExpiresIn = new Date(now + (1000 * accessTokenExpired)); // 30분
        Date refreshTokenExpiresIn = new Date(now + (1000 * refreshTokenExpired)); // 14일

        String accessToken = Jwts.builder()
            .setSubject(authentication.getName())
            .claim("auth", "USER")
            .setExpiration(accessTokenExpiresIn)
            .signWith(secret, SignatureAlgorithm.HS256)
            .compact();

        log.info(parseClaims(accessToken).toString());

        String refreshToken = Jwts.builder()
            .setExpiration(refreshTokenExpiresIn)
            .signWith(secret, SignatureAlgorithm.HS256)
            .compact();

        log.info("accessToken: {}", accessToken);
        return TokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    /**
     * 토큰에서 유저 정보 추출
     */
    public Authentication getAuthentication(String accessToken) {
        Claims claims = parseClaims(accessToken);

        if (claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }
        log.info("claims: {}", claims);
        Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get("auth").toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        log.info("authorities: {}", authorities);
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    /**
     * 토큰 정보 검증
     */
    public boolean validateToken(String token) {
        log.info("validateToken start");
        log.info("token: {}", token);
        try {
            Jwts.parserBuilder().setSigningKey(secret).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            // refresh token 활용해서 재발급
            log.info("Expired JWT Token", e);
            throw e;
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder()
               .setSigningKey(secret)
               .build()
               .parseClaimsJws(accessToken)
               .getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }

    public String getMemberEmail(String refreshToken) {
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secret)
            .build()
            .parseClaimsJws(refreshToken)
            .getBody();
        return claims.getSubject();
    }
}
  • generateToken : 만들어진 authentication을 활용해서 TokenInfo을 만든다
  • getAuthentication : accessToken을 확인하고 인증 정보를 반환해준다.
  • validateToken : 토큰의 valid를 검증한다.
    • 각 Exception들은 RuntimeError를 상속하고 있는데, ExpiredJwtException은 추후 accessToken 만료 시나리오에서 쓰이기 때문에 catch 하지 않고 throw 해준다.
  • getClaims : 클레임을 반환한다.

 

JwtAuthenticationFilter


@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Value("${jwt.secret}")
    private String secret;
    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper;
    private static final String BEARER = "Bearer ";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        log.info("doFilterInternal start");
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

        // access token이 있고, BEARER로 시작한다면
        if (authorization != null && authorization.startsWith(BEARER)) {
            String token = authorization.substring(BEARER.length());
            log.info("token: {}", token);
            // token을 검증한다.
            try {
                if (jwtTokenProvider.validateToken(token)) {
                    // token이 유효하다면
                    // token으로부터 유저 정보를 받아온다.
                    Authentication authentication = jwtTokenProvider.getAuthentication(token);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e) {
                log.info("ExpiredJwtException");
                // access token이 만료되었다면
                // 에러를 보낸다
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                Map<String, String> error = new HashMap<>();
                error.put("message", "access token expired");
                error.put("code", "401");
                error.put("status", "UNAUTHORIZED");
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(error));
                return;
            }

        }

        filterChain.doFilter(request, response);
    }
}
  • OncePerRequestFilter 🆚 GenericFilterBean
    • GenericFilterBean : 매 서블릿 마다 호출된다
    • OncePerRequestFilter : RequestDispatcher 클래스에 의해 다른 서블릿으로 dispatch 되더라도, Filter가 실행되지 않는다.
    • 추후 Oauth 구현을 위해 OncePerRequestFilter로 설정하였다.
  • doFilterInternal
    • Header에서 Authorization을 활용해 accessToken을 검증한다
    • 엑세스 토큰이 만료되었을 경우 Exception을 캐치하여 response에 에러 메세지를 반환해준다
    • 엑세스 토큰이 정상적일 경우, SecurityContextHolder에 Authenticaton을 가져와 설정해준다.
    • 엑세스 토큰이 없을 경우 설정 없이 필터 체인을 거치게 된다.

 

SecurityConfig


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .httpBasic().disable()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            .antMatchers("/login/member").permitAll() // 해당 API 모든 요청 허가
            .antMatchers("/login/token/reissue").permitAll() // 해당 API 모든 요청 허가
            .antMatchers("/login/test").authenticated()
//            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    // 그 외 나머지 요청은 인증 필요
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}
  • passwordEncoder : 단방향 암호화 문제를 해결하기 위한 팩토리패턴을 활용해 빈으로 등록하였다. 추후 암호화 알고리즘 변경시에도 적용하기 좋다
  • Config를 통해 필터체인을 등록한다. 이때 내가 만든 JwtAuthenticationFilter 가 시큐리티보다 먼저 적용될 수 있도록 해야한다.
  • Cors 정책으로 인해 HTTP OPTIONS는 모두 허용 해주었다.
  • /login/member, /login/token/reissue는 권한이 없는 사용자의 요청이므로, 해당 요청에 대해서는 모든 요청을 허락 해주어야 한다.
  • /login/test 요청은 authenticated를 통해 인가 확인하고 실행하도록 한다

 

 

 

Spring Controller, Service, Repository 구현

LoginMemberController


@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/login")
public class LoginMemberController {

    @Value("${cors.frontend}")
    private String referer;

    private Long refreshTokenExpired;

    private final LoginMemberService loginMemberService;

    @PostMapping("/member")
    public TokenInfo loginTest(@RequestBody LoginMemberRequestDto loginMemberRequestDto
        , HttpServletResponse response) throws IOException {
        log.info("memberLoginRequestDto: {}", loginMemberRequestDto);
        TokenInfo token = loginMemberService.login(loginMemberRequestDto);
        log.info("token: {}", token);
        Cookie refreshToken = new Cookie("refreshToken", token.getRefreshToken());
        response.setHeader("Set-Cookie",
            "refreshToken=" + token.getRefreshToken() + "; Path=/; HttpOnly; Secure; Max-Age=" + refreshTokenExpired);
        return token;
    }

    @PostMapping("/test")
    public String test() {
        return "test";
    }

    @GetMapping("/token/reissue")
    public Object reissueAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {

        log.info("reissueAccessToken start");
        Cookie refreshToken = WebUtils.getCookie(request, "refreshToken");
        log.info("refreshToken: {}", refreshToken);
        Object responseJson = loginMemberService.reissue(refreshToken.getValue());
        log.info("tokenInfo: {}", responseJson);
        if (responseJson instanceof TokenInfo) {
            TokenInfo tokenInfo = (TokenInfo) responseJson;
            response.setHeader("Set-Cookie",
                "refreshToken=" + tokenInfo.getRefreshToken() + "; Path=/; HttpOnly; Secure; Max-Age=" + refreshTokenExpired);
        }
        return responseJson;
    }


}

 

 

 
  • loginTest : “/login/member” - 로그인 이메일, 비밀번호를 활용해서 로그인을 시도하고, 로그인이 성공할 경우 response에 Set-Cookie 헤더를 설정하여 refreshToken을 반환해준다.
  • test : 엑세스 토큰의 만료 여부를 체크하여, 실제 토큰이 잘 동작하는지 체크한다.
  • reissueAccessToken : “/login/token/reissue” - 쿠키에 담긴 refreshToken을 활용해서 토큰을 재발급 한다.

 

LoginMemberService


@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class LoginMemberService {

    private final LoginMemberRepository loginMemberRepository;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtTokenProvider jwtTokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    @Transactional
    public TokenInfo login(LoginMemberRequestDto loginMemberRequestDto) {
        log.info("login start");
        String memberEmail = loginMemberRequestDto.getMemberEmail();
        String password = loginMemberRequestDto.getPassword();

        // 1. MemberEmail, Password로 Member 조회
        Member member = loginMemberRepository.findByEmail(memberEmail)
            .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다."));
        if (!member.getPassword().equals(password)) {
            throw new RuntimeException("비밀번호가 일치하지 않습니다.");
        }


        // 2. 토큰 생성
        TokenInfo tokenInfo = createNewToken(memberEmail, password);

        // 3. 저장소에 Refresh Token 저장
        saveRefreshToken(tokenInfo, member);
        log.info("end generateToken");
        return tokenInfo;
    }

    @Transactional
    public Object reissue(String refreshToken) {
        log.info("reissue start");
        if (!jwtTokenProvider.validateToken(refreshToken)) {
            throw new RuntimeException("Refresh Token이 유효하지 않습니다.");
        }

        // Refresh Token Rotation 기법 사용, 유효한 Refresh Token인지 확인
        // 유효하지 않을 경우 Exception 발생
        // 유효할 경우 Refresh Token 가져와서 사용
        RefreshToken refreshTokenEntity = refreshTokenRotation(refreshToken);
        if (refreshTokenEntity.getStatus() == RefreshTokenStatus.INVALID) {
            return new UsedTokenError("토큰 탈취 에러", HttpStatus.BAD_REQUEST.toString(),"Refresh Token이 이미 사용되었습니다.");
        }
        refreshTokenEntity.updateStatus(RefreshTokenStatus.INVALID);

        // 3. 토큰 생성
        TokenInfo tokenInfo = createNewToken(refreshTokenEntity.getMember().getEmail(), refreshTokenEntity.getMember().getPassword());

        // 4. 저장소에 Refresh Token 저장
        saveRefreshToken(tokenInfo, refreshTokenEntity.getMember());

        log.info("tokenInfo: {}", tokenInfo);
        return tokenInfo;
    }

    /**
     * Refresh Token Rotation 기법 적용
     */
    private RefreshToken refreshTokenRotation(String refreshToken) {
        Optional<RefreshToken> token = refreshTokenRepository.findById(refreshToken);
        if (token.isEmpty()) {
            throw new RuntimeException("Refresh Token이 유효하지 않습니다.");
        }
        RefreshToken refreshTokenEntity = token.get();
        if (refreshTokenEntity.getStatus() == RefreshTokenStatus.INVALID) {
            // 탈취된 토큰이 활용된 것이므로, 관련 멤버의 모든 Refresh Token을 무효화한다.
            // refresh 토큰은 한번만 사용할 수 있도록 한다.
            refreshTokenRepository.updateAllByMember(refreshTokenEntity.getMember(), RefreshTokenStatus.INVALID);
        }
        return refreshTokenEntity;
    }

    /**
     * 새로운 토큰 생성
     */
    private TokenInfo createNewToken(String memberEmail, String password) {
        log.info("createNewToken start");
        // 1. Refresh Token으로 MemberEmail 조회
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberEmail, password);
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        log.info("authentication: {}", authentication);
        // 2. 새로운 토큰 생성 - refresh token도 새로 생성해서 나가게 된다.
        return jwtTokenProvider.generateToken(authentication);
    }

    /**
     * Refresh Token 저장
     */
    private void saveRefreshToken(TokenInfo tokenInfo, Member refreshTokenEntity) {
        log.info("saveRefreshToken start");
        RefreshToken newRefreshToken = new RefreshToken(tokenInfo.getRefreshToken(), RefreshTokenStatus.VALID,
            refreshTokenEntity);
        refreshTokenRepository.save(newRefreshToken);
    }


}
  • login
    • 아이디 비밀번호를 활용해 토큰을 발급한다.
    • 발급된 토큰의 Refresh Token을 DB에 저장한다.
  • reissue
    • 유효한 RefreshToken일 경우 재발급 해주는 로직이다.
    • 사용된 RefreshToken은 INVALID로 변경시켜 주며, 새로운 토큰을 Refresh와 Access를 함께 보내준다. (RTR 적용을 위해)
    • 저장소에 새로 발급된 RefreshToken을 저장한다
  • refreshTokenRoation
    • DB에서 refreshToken이 존재하는지 확인한다.
    • 존재한다면 refreshToken이 유효한지 status를 확인한다.
    • 유효하지 않은 refreshToken이 요청올 경우 관련 멤버의 모든 RefreshToken을 INVALID로 변경한다.
    • 트랜잭션 안에서 작동하기 때문에, 에러를 반환하지 않고 정상로직으로 작동하며 refreshTokenEntity 값이 INVALID 이기 때문에, 해당 값을 활용하여 Error객체를 반환하도록 설정한다.
  • createNewToken
    • 이메일과 패스워드를 활용해 새로운 토큰을 발급해준다.
  • saveRefreshToken
    • Repository에 토큰을 저장한다.
💡
추후 RTR 방식을 변경할 것이다. 현재는 refreshToken이 유효한 경우와 유효하지 않은 경우가 모두 DB에 저장되어 있다. 하지만 이 중에서 유효한 refreshToken을 Redis에 저장시키면 유효한 사용자의 재발급 요청은 더 빠르게 처리될 수 있을 것이다. 사용된 토큰은 redis에서 삭제하면 된다. 유효하지 않은 refreshToken으로 요청이 온다면 RDBMS에 저장된 데이터베이스에서 해당 토큰을 찾고, 만약에 해당 토큰이 저장되어 있다면 이미 사용된 토큰이므로 레디스에서 해당 유저와 관련된 모든 토큰을 만료시킬 수 있을 것이다. 이때 레디스에 저장하는 key는 token이 아니라 userEmail을 기반으로 해야할 것이다. 토큰 만료 시나리오에서 userEmail을 사용해 찾아야하기 때문이다.

 

LoginMemberRepository


@Repository
public interface LoginMemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}
  • findByEmail : 이메일을 통해 멤버를 찾아온다

 

RefreshTokenRepository


public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
    @Modifying(clearAutomatically = true)
    @Query("update RefreshToken r set r.status = :refreshTokenStatus where r.member = :member")
    void updateAllByMember(@Param("member") Member member, @Param("refreshTokenStatus") RefreshTokenStatus refreshTokenStatus);
}
  • @Modifying : 쿼리 수행 후 엔티티매니저의 1차캐시를 삭제해준다.
  • member에 해당하는 모든 RefreshToken을 Invalid로 처리한다.

 

 

 

Refresh Token Rotation 시나리오


1. 두개의 계정으로 발급한 리프레시 토큰을 만들어 둔다.

2. 하나의 토큰을 사용해 만료 시키고 새로운 토큰을 발급한다.

3. INVALUD된 토큰으로 재요청을 보내면 update query가 나간다

4. DB를 확인하면 모든 토큰이 INVALID가 되어있는 것을 알 수 있

 

같은 member에 대해서 reissue 요청시 jpa쿼리 2번 나가는 상황 발생

 
 
  • 같은 멤버이지만, id로 한번 email로 한번 총 2번 조회되는 현상 발견
TokenInfo tokenInfo = createNewToken(refreshTokenEntity.getMember().getEmail(), refreshTokenEntity.getMember().getPassword());
  • 영속성 객체는 ID로 관리되는데, email을 통해 불러오려고 하니, 영속성 객체가 아닌 DB를 참고하여 가지고 왔다
  • 이후 CustomUserDetailsService 를 수정하여 쿼리를 한번으로 줄일 수 있었다
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final LoginMemberRepository loginMemberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        return loginMemberRepository.findById(Long.valueOf(userId)) // 해당 부분이 findByEmail로 되어 있었음
            .map(this::createUserDetails)
            .orElseThrow(() -> new UsernameNotFoundException(userId + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    private UserDetails createUserDetails(Member member) {
        return User.builder()
                .username(member.getId().toString()) // username에 email을 넣어주던 것을 Id로 수정하였다.
                .password(passwordEncoder.encode(member.getPassword()))
                .roles(member.getRoles().toArray(new String[0]))
                .build();
    }
}
  • Spring Data JPA 에서도 join fetch를 사용하여 member를 한번에 불러오도록 하였다.
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {

    @Query("select r from RefreshToken r join fetch r.member where r.refreshToken = :refreshToken")
    Optional<RefreshToken> findFetchByRefreshToken(@Param("refreshToken") String refreshToken);
}
  • 이후 재발급 서비스에서도 Email이 아닌 Id를 사용하도록 하였으며, JwtProvider에서 sub에도 email이 아니라 id를 넣도록 하였다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class LoginMemberService {

    private final LoginMemberRepository loginMemberRepository;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtTokenProvider jwtTokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    @Transactional
    public Object reissue(String refreshToken) {
        log.info("reissue start");
        if (!jwtTokenProvider.validateToken(refreshToken)) {
            throw new RuntimeException("Refresh Token이 유효하지 않습니다.");
        }

        // Refresh Token Rotation 기법 사용, 유효한 Refresh Token인지 확인
        // 유효하지 않을 경우 Exception 발생
        // 유효할 경우 Refresh Token 가져와서 사용
        RefreshToken refreshTokenEntity = refreshTokenRotation(refreshToken);
        if (refreshTokenEntity.getStatus() == RefreshTokenStatus.INVALID) {
            return new UsedTokenError("토큰 탈취 에러", HttpStatus.BAD_REQUEST.toString(), "Refresh Token이 이미 사용되었습니다.");
        }
        refreshTokenEntity.updateStatus(RefreshTokenStatus.INVALID);

        log.info(jwtTokenProvider.getMemberId(refreshToken));
        // 3. 토큰 생성
        TokenInfo tokenInfo = createNewToken(jwtTokenProvider.getMemberId(refreshToken),
            refreshTokenEntity.getMember().getPassword()); // createNewToken 부분 ID로 수정

        // 4. 저장소에 Refresh Token 저장
        saveRefreshToken(tokenInfo, refreshTokenEntity.getMember());

        log.info("tokenInfo: {}", tokenInfo);
        return tokenInfo;
    }

    /**
     * Refresh Token Rotation 기법 적용
     */
    private RefreshToken refreshTokenRotation(String refreshToken) {
        log.info("refreshTokenRotation start");
        Optional<RefreshToken> token = refreshTokenRepository.findFetchByRefreshToken(refreshToken);
        if (token.isEmpty()) {
            throw new RuntimeException("Refresh Token이 유효하지 않습니다.");
        }
        log.info("token: {}", token);
        RefreshToken refreshTokenEntity = token.get();
        log.info("refreshTokenEntity: {}", refreshTokenEntity);
        if (refreshTokenEntity.getStatus() == RefreshTokenStatus.INVALID) {
            // 탈취된 토큰이 활용된 것이므로, 관련 멤버의 모든 Refresh Token을 무효화한다.
            // refresh 토큰은 한번만 사용할 수 있도록 한다.
            refreshTokenRepository.updateAllByMember(refreshTokenEntity.getMember(), RefreshTokenStatus.INVALID);
        }
        log.info("end refreshTokenRotation");
        return refreshTokenEntity;
    }

    /**
     * 새로운 토큰 생성
     */
    private TokenInfo createNewToken(String id, String password) {
        log.info("createNewToken start");
        // 1. Refresh Token으로 MemberEmail 조회
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(id,
            password);
        log.info("authenticationToken: {}", authenticationToken);
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        log.info("authentication: {}", authentication);
        // 2. 새로운 토큰 생성 - refresh token도 새로 생성해서 나가게 된다.
        return jwtTokenProvider.generateToken(authentication);
    }

		/**
     * createNewToken 오버로딩
     */
    private TokenInfo createNewToken(Long id, String password) {
        return createNewToken(id.toString(), password); // id를 string으로 바꿔서 넣도록 함
    }

    /**
     * Refresh Token 저장
     */
    private void saveRefreshToken(TokenInfo tokenInfo, Member member) {
        log.info("saveRefreshToken start");
        RefreshToken newRefreshToken = new RefreshToken(tokenInfo.getRefreshToken(), RefreshTokenStatus.VALID, member);
        refreshTokenRepository.save(newRefreshToken);
    }


}
 
 

 

반응형
Posted by 진우식
,
반응형

https://quantgnu.tistory.com/6

 

퀀트 트레이딩 프로젝트

https://colab.research.google.com/drive/1c6Pz4rRr5ct1-kZrXIAca3PEk0PKuXMl?hl=ko#scrollTo=Zu9Q1JTMMy-l 퀀트기반 추천주 프로그램.ipynb Colaboratory notebook colab.research.google.com 군대에서 사지방을..

quantgnu.tistory.com

 

1. MySQL 설치

MySQL 설치와 관련된 내용은 구글링을 하면 찾아볼 수 있다.

무료로 이용할 수 있으면서 이전에 사용해본 경험이 있었기 때문에 MySQL로 선택을 하게 되었고

현재 진행할 프로젝트의 경우에는 AI와 관련된 내용보다는 주로 재무데이터나 가격데이터를 일단위로 활용하기 때문에, 데이터가 많지 않고 데이터간의 연관성이 중요해서 RDBMS를 선택하였다.

 

2. 외부 접속 계정 추가

create user 'id'@'%' identified by 'password';
FLUSH privileges;
grant all privileges on *.* to 'id'@'%';
FLUSH privileges;

외부 접속을 허용해 줄 계정을 생성하고, 해당 계정에 모든 권한을 부여해 준다.

colab 환경을 사용할 것이기 때문에, 로컬 컴퓨터의 데이터베이스에 접속하기 위해 설정 해 주는 것이다.

 

간단하게 적어놨지만, 실제로 필자는 해당 과정을 하는데 많은 시간이 소모되었다.

 

문제점 - 공유기 사용

공유기를 사용하게 되면, 공인ip와 내부ip가 존재하게 되는데 이때 내부ip가 아닌 공인ip를 이용해서 Mysql에 접속해야 하고, MySQL에 접속하기 위한 Port또한 열어 주어야 한다. 그리고 해당 포트에 접근 했을때 특정 내부ip와 연결시키기 위해 pc에서 사용하는 ip를 고정시켜주는 작업도 필요하다. 해당 내용에 대해서 너무 많이 다루게 되면 조금은 포스팅의 내용을 벗어나게 되는 것 같아 구글링을 참고하길 바란다.

 

또한 공유기를 하나가 아닌 두개 이상을 사용하는 경우가 있다. 필자의 경우에도 skt에서 인터넷을 설치해주고 가며 모뎀을 하나 설치해주고 갔는데, 해당 모뎀은 유선공유기이기 때문에 그것 또한 설정이 필요했다. 무선공유기 하나만 설정해주게 되면 제대로된 접속이 안될 수 있기 때문에 자신이 이용하고 있는 인터넷 통신사에서 설치해준 모뎀을 잘 확인하여 3306포트를 잘 열어주는 것이 필요하다.

 

2 - 1. MySQL 경로 변경 (필수X)

C드라이버의 용량이 부족한 관계로, 여유분의 SSD에서 데이터베이스를 실행 시키기 위해

경로가 C드라이브로 되어 있던 것을 E드라이브로 변경하려고 한다.

윈도우 작업관리자를 실행하여 먼저 MySQL을 찾아 중지시킨다.

 

C:\ProgramData\MySQL\MySQL Server 8.0\Data 폴더를 그대로 복사하여 E드라이브 옮긴다.

MySQL 폴더를 만들어 해당 폴더 안에 Data 파일을 복사해준다

C:\ProgramData\MySQL\MySQL Server 8.0\my.ini 파일을 메모장(관리자권한)으로 열어준다

이후 datadir을 찾아서 원래 것을 주석 처리하고 새로 옮긴 곳으로 경로를 저장한다.

그리고 MySQL을 다시 실행시켜 준다.

 

이렇게 한 뒤 WorkBench를 통해 경로를 확인해 보면

경로가 바뀌어 있는 것을 알 수 있다.

 

2 - 2. MySQL 데이터 복사 (필수 X)

이 프로젝트는 군대시절 사지방에서 하던 프로젝트로, 예전의 로컬PC에서 서버가 돌아가고 있었다.

전역을 한 뒤 컴퓨터의 파워 문제로 아예 성능 향상을 같이 하기 위해 pc를 바꿨고, 이후 MySQL을 설치하지 않고 있다가 이번에 설치하게 되어 이전PC에 있던 데이터를 ibd파일을 이용해 복사해야 하는 상황이 발생하였다.

 

하지만 기존 데이터가 없어도 크롤링을 이용해 수집하기 때문에, 이후의 과정만 따라하게 된다면, 새로운 데이터를 수집하는데는 문제가 없을 것이다. (하지만 과거의 데이터까지 이용하는 것이 더 많이 분석할 수 있기 때문에 기존의 데이터를 활용하려고 하는 것이다)

예전 파일을 확인해보면 이렇게 ibd파일이 생성되어 있는 것을 알 수 있다.

하지만 frm파일은 따로 없었기 때문에 테이블의 구조는 colab에 저장해두었던 sql을 이용해 복원하였다.

create table ktbl (
	kind varchar(4) Not Null,
    k_name varchar(20) Not Null,
    PRIMARY KEY (kind)
);

create table comtbl (
	c_code char(6) Not Null,
    c_name varchar(20) Not Null,
    c_kindlarge char(2) Not Null,
    c_kindsmall char(4) Not Null,
    PRIMARY KEY (c_code),
    FOREIGN KEY (c_kindlarge) REFERENCES ktbl (kind),
    FOREIGN KEY (c_kindsmall) REFERENCES ktbl (kind)
);

create table ftbl (
	c_code char(6) Not Null,
    f_date DATE Not Null,
    sales BIGINT NOT Null,
    gm BIGINT NOT Null,
    ni BIGINT NOT Null,
    asset BIGINT NOT Null,
    ca BIGINT NOT Null,
    cl BIGINT NOT Null,
    issued_shares BIGINT NOT Null,
    bps INT NOT Null,
    EPS INT NOT Null,
    PRIMARY KEY (c_code, f_date),
    FOREIGN KEY (c_code) REFERENCES comtbl (c_code)
);

create table prtbl(
	c_code char(6) Not Null,
	p_date DATE Not Null,
    price int not null,
    PRIMARY KEY (c_code, p_date),
    FOREIGN KEY (c_code) REFERENCES comtbl (c_code)
);

총 4개의 테이블로 이루어져 있으며, 해당 테이블의 자세한 내용은 해당 게시글의 가장 위 링크를 통해 프로젝트 시작으로 들어가보면 깃허브 링크가 있다, 해당 링크에 colab 문서를 통해서 작성해 둔 조금 더 시각적으로 표현된 markdown이 존재한다.

SET foreign_key_checks = 0;
ALTER TABLE comtbl DISCARD TABLESPACE;
ALTER TABLE ktbl DISCARD TABLESPACE;
ALTER TABLE ktbl DISCARD TABLESPACE;
ALTER TABLE prtbl DISCARD TABLESPACE;

외래 키가 있어서 삭제가 안될 수 있기 때문에, 우선 외래키 확인 조건을 false로 놔두고 ibd파일을 모두 삭제해준다.

기존 파일 -> 현재 경로로 복사

그리고 원래 가지고 있던 파일들을 복사하여 quantdb안에 넣어준다.

ALTER TABLE comtbl IMPORT TABLESPACE;
ALTER TABLE ktbl IMPORT TABLESPACE;
ALTER TABLE ftbl IMPORT TABLESPACE;
ALTER TABLE prtbl IMPORT TABLESPACE;
SET foreign_key_checks = 1;

그리고 옮긴 ibd파일을 적용시킨 후, 외래키 확인 조건을 True로 변경해주면 설정이 끝난다.

이렇게 설정이 끝난 뒤에는 colab에서 sql에 접근 할 수 있게 된다.


이후에는 Worbench는 거의 이용하지 않고 colab을 통해서 데이터를 insert, select 할 것이기 때문에 이번 포스팅은 여기서 마치고 다음 Part2로 찾아오겠습니다.

반응형
Posted by 진우식
,
반응형

https://colab.research.google.com/drive/1c6Pz4rRr5ct1-kZrXIAca3PEk0PKuXMl?hl=ko#scrollTo=Zu9Q1JTMMy-l 

 

퀀트기반 추천주 프로그램.ipynb

Colaboratory notebook

colab.research.google.com

군대에서 사지방을 이용해서 시작했던 퀀트를 기반으로한 추천주 프로그램이다.

미완성으로 남겨뒀던 것을 이번에 코드를 다시 보며 복습하고, 포스팅 하기 위해 가져왔다.

 

코드를 하나씩 보며 조금씩 수정해나가기도 할 것이고, 이후 포스팅을 통해 해당 프로젝트에 사용된 기술들을 적으려고 한다.

반응형
Posted by 진우식
,
반응형

안녕하세요 지누식입니다.

오늘은 여러분께 조금은 어려운 인공지능에 대해서 소개해드리려고 합니다.



현재 많은 분야에서 이용되고 있는 인공지능은, 이미 일반인들에게 많은 정보가 공개되어 있다고 해도 과언이 아닙니다.

구글에서 tensorflow를 공개하면서 많은 부분을 일반인들이 이용할 수 있게 되었고

이를 더 편리하게 keras에서 각종 모듈을 제공하고 있습니다.



오늘은 keras에서 가장 단순한 Dense 레이어와 재무제표, 주가를 이용하여 1년 뒤의 주가를 예측해보는 실험을 진행해보려고 합니다.

우선 두 가지의 데이터셋이 필요합니다.

1. 재무제표 데이터 (이중컬럼을 이용) - total_data

2. 주가 데이터 - total_price



1번과 2번 모두 크롤링을 이용해 구할 수 있지만, 1번의 경우에는 3~4년 치의 데이터밖에 구할 수 없습니다.

따라서 따로 결제하거나 하셔야 하지만, 일단 저는 가지고 있는 데이터가 있음으로 이용하도록 하겠습니다

또한 2번 데이터의 경우는 크롤링으로 쉽게 해결할 수 있기 때문에 나중에 포스팅으로 다룰 예정입니다..

 

for year in range(2000,2019):
    code_data1 = total_data[year]['총자산(평균)(원)'].dropna().index.tolist()
    code_data2 = total_data[year-1]['총자산(평균)(원)'].dropna().index.tolist()
    code_data3 = total_data[total_data[year-1]['매출액(원)'] != 0].index.tolist()
    code_data = list(set(code_data1).intersection(code_data2))
    code_data = list(set(code_data).intersection(code_data3))
    
    
    #가격정보 저장 - T_data
    price_data = total_price['%s-05' % str(year):'%s-05' % str(year+1)][code_data]
    price_data = price_data.dropna(axis=1)
    price_data = (price_data - price_data.loc[price_data.index[0]] )/price_data.loc[price_data.index[0]]
    MachineLearning_T_data = pd.DataFrame(index = price_data.columns)
    MachineLearning_T_data['End'] = price_data.loc[price_data.index[-1]]
    MachineLearning_T_data.to_excel(r'경로\머신러닝 T_data - %d.xlsx' %year)
    
    #실용데이터 저장 X_data
    practical_data = pd.DataFrame(index = code_data)
    data = total_data.loc[code_data][year]
    pre_data = total_data.loc[code_data][year-1]
    
    practical_data['ROA_Absolute'] = data['당기순이익(원)']/data['총자산(평균)(원)']
    practical_data['Cashflow_Asset'] = data['현금흐름(원)']/data['총자산(평균)(원)']
    practical_data['GPA'] = data['매출총이익(원)'] / data['총자산(평균)(원)']
    practical_data['Profit_increase'] = (data['매출액(원)'] - pre_data['매출액(원)']) / pre_data['매출액(원)']

    practical_data[practical_data[practical_data.columns] == float('inf')] = 0
    practical_data = practical_data.fillna(0)
    
    practical_data.to_excel(r'경로\실용데이터 - %d.xlsx' %year)

우선 전체 코드를 먼저 입력해두고 하나씩 파헤치는 식으로 설명해드리겠습니다.

for year in range(2000,2019):
    code_data1 = total_data[year]['총자산(평균)(원)'].dropna().index.tolist()
    code_data2 = total_data[year-1]['총자산(평균)(원)'].dropna().index.tolist()
    code_data3 = total_data[total_data[year-1]['매출액(원)'] != 0].index.tolist()
    code_data = list(set(code_data1).intersection(code_data2))
    code_data = list(set(code_data).intersection(code_data3))

먼저 앞부분입니다.

for 문을 통해서 year값이 2000~2018까지 1씩 증가하도록 설정 해줍니다.

그 후 code_data1 , code_data2, code_data3 을 통해서 nan 값을 선별해 줍니다.



총자산(평균)(원)에 들어있는 값이 nan 이라면 다른 값에도 nan이 많이 들어있었거나 값이 없었기 때문에

총자산(평균)(원) 값이 nan인 index들을 dropna를 통해 없애주었습니다.



또 단순히 1년이 아니라, 전년도의 수치와 비교도 해야 하기 때문에, 전년도 데이터가 없는 것 또한 dropna 해주었습니다.



그리고 마지막으로 제가 데이터를 얻은 곳이 fnguide라는 곳인데, 상장 전의 정보도 포함하고 있어서 매출액이 0인 경우가 가끔 있었습니다.

그래서 total_data 중에서 전년도의 매출액이 0이 아닌 것만 code_data3에 저장했습니다.



그 후에 intersection을 통해 3개의 데이터 중에서 중복되는 것만 합쳤습니다.

 

    #가격정보 저장 - T_data
    price_data = total_price['%s-05' % str(year):'%s-05' % str(year+1)][code_data]
    price_data = price_data.dropna(axis=1)
    price_data = (price_data - price_data.loc[price_data.index[0]] )/price_data.loc[price_data.index[0]]
    MachineLearning_T_data = pd.DataFrame(index = price_data.columns)
    MachineLearning_T_data['End'] = price_data.loc[price_data.index[-1]]
    MachineLearning_T_data.to_excel('경로\머신러닝 T_data - %d.xlsx' %year)

그 후에는 가격정보를 정리했습니다.


ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ왜 5월에서 5월까지인가?ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

가격정보의 경우 05~04월을 이용하는 게 일반적입니다. 

2011년의 재무제표의 경우 2012년 4월 초에 공시가 됩니다. 

그래서 2012년 5월쯤부터 이용할 수 있다고 가정하는 것이죠. 

하지만 저의 경우에는 1년 동안 보유한 주식의 수익률을 예측하여, 이 예측값이 잘 맞아떨어진다면 

나중에 LSTM을 이용할 때 하나의 변수로 넣어주려고 했습니다. 

그리하여 LSTM을 이용할 때 06~05월의 데이터를 예측하기 위해, 당해년도의 05월의 예측값을 이용하기 위해 05~05월로 지정하게 되었습니다. 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ


여기서는 dropna에 axis=1값을 주었습니다.

기간의 초기와 마지막에 가격 데이터가 없는 경우가 있는데, 이를 방지하기 위함입니다.



또 price_data를 정규화해 주었습니다.

price_data의 경우 모든 주식마다 가격이 다 다르기 때문에, 정규화를 해주지 않으면 가격이 비싼 주식이 더 많은 영향을 미치게 됩니다. 따라서 정규화를 통해 각각의 주식들을 비슷한 정도로 모델에 반영되게 만들어 주었습니다.



이후 End값을 주기 위해 MachineLearning_T_data라는 데이터 프레임을 만들고

MachineLearning_T_data의 End 컬럼에 price_data에 있는 마지막 값을 넣어주었습니다.

이렇게 저장한 데이터 프레임을 경로를 지정하여 '머신러닝 T_data.xlsx'로 저장해줍니다.

 

    #실용데이터 저장 X_data
    practical_data = pd.DataFrame(index = code_data)
    data = total_data.loc[code_data][year]
    pre_data = total_data.loc[code_data][year-1]
    
    practical_data['ROA_Absolute'] = data['당기순이익(원)']/data['총자산(평균)(원)']
    practical_data['Cashflow_Asset'] = data['현금흐름(원)']/data['총자산(평균)(원)']
    practical_data['GPA'] = data['매출총이익(원)'] / data['총자산(평균)(원)']
    practical_data['Profit_increase'] = (data['매출액(원)'] - pre_data['매출액(원)']) / pre_data['매출액(원)']

    practical_data[practical_data[practical_data.columns] == float('inf')] = 0
    practical_data = practical_data.fillna(0)
    
    practical_data.to_excel('경로\실용데이터 - %d.xlsx' %year)

제가 사용한 모든 데이터를 여기에 가져오진 않았습니다.

단순하게 이용하기 위해 4개의 지표만 사용하고 이후에 1개를 더 추가하여 볼 예정입니다.



4가지의 지표는 ROA, Cashflow, GPA, Profit_increase입니다.

ROA, GPA, Profit_increase는 수익성을 대변하며

Cashflow는 실제로 돈이 들어오고 있는가를 보여주는 지표입니다.

이것들을 이용하기 위해 각종 계산식을 이용해 주었고, inf값이 들어가는 경우는 제외하고, nan값이 들어가는 경우는 0으로 지정해주었습니다.

nan값이 들어가는 경우를 0으로 지정해준 이유는 nan값의 경우 영향을 미치면 안 되기 때문에 가중치와 x값이 곱해져서 결국 딥러닝을 할 때 0이 더해지기 때문입니다.

 

이렇게 코드를 하고 나면, 이런 데이터들이 나오게 됩니다.

이제 이 데이터들을 이용하여 1년 후의 주가를 예측해보려고 합니다.

 

오늘은 google colab을 이용하여 딥러닝을 할 예정입니다.

google colab은 내 컴퓨터가 아니라, 구글에서 제공하는 컴퓨터를 이용하기 때문에 훨씬 더 빨리 학습시킬 수 있다는 장점이 있습니다.

본인의 컴퓨터의 성능이 엄청나지 않다면 구글 코랩을 이용하시는걸 추천해 드리겠습니다.



이 google colab에 우선 아까 만들었던 data들을 다 집어 넣어주고 시작하겠습니다.

from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
import pandas as pd
import numpy as np

# 데이터셋 생성하기
for num, year in enumerate(range(2000,2018)):
    x_train = pd.read_excel('실용데이터 - %d.xlsx' %year, index_col = 0)
    t_train = pd.read_excel('머신러닝 T_data - %d.xlsx' %year, index_col = 0)
    merged_train = pd.DataFrame(index = t_train.index)
    merged_train[x_train.columns] = x_train[x_train.columns]
    merged_train[t_train.columns] = t_train[t_train.columns]
    if num == 0:
        total_train = merged_train
    else:
        total_train = pd.concat([total_train,merged_train])

# 데이터 나누기
x_train = total_train[total_train.columns[:-1]].values
t_train = total_train[total_train.columns[-1]].values

# validation data 생성
x_val = x_train[ int(len(t_train) * 0.9) : ]
t_val = t_train[ int(len(t_train) * 0.9) : ]
    #array로 변경
t_val = t_val.reshape(len(t_val),1)

# train data 재생성 (validation data를 뺀것)
x_train = x_train[ : int(len(t_train) * 0.9) ]
t_train = t_train[: int(len(t_train) * 0.9) ]
    #array로 변경
t_train = t_train.reshape(len(t_train),1)

x_test = pd.read_excel('실용데이터 - 2018.xlsx', index_col = 0)
t_test = pd.read_excel('머신러닝 T_data - 2018.xlsx', index_col = 0)
x_test = x_test.loc[t_test.index].values
t_test = t_test['End'].values.reshape(len(t_test),1)

# 모델 만들기
Model_End = Sequential()

Model_End.add( Dense(64, input_dim=len(x_train.T), activation = 'relu') )
Model_End.add( Dense(64, activation = 'relu') )
Model_End.add( Dense(1, activation = 'linear') )

Model_End.compile( loss = 'mse', optimizer = 'rmsprop') # mean_squared_error = mse , optimizer는 Adam과 rmsprop 비교해보기

Model_End.summary()

# 모델 학습시키기
Model_End.fit(x_train, t_train, validation_data = (x_val, t_val) , batch_size = 10, epochs = 100)

# 모델 평가하기
loss = Model_End.evaluate(x_test, t_test, batch_size=10)
print('loss : ' + str(loss))

이렇게 코드를 만들고 나니 꽤나 긴 코드가 되었는데요. 차근차근! 설명해드리겠습니다.

from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
import pandas as pd
import numpy as np

# 데이터셋 생성하기
for num, year in enumerate(range(2000,2018)):
    x_train = pd.read_excel('실용데이터 - %d.xlsx' %year, index_col = 0)
    t_train = pd.read_excel('머신러닝 T_data - %d.xlsx' %year, index_col = 0)
    merged_train = pd.DataFrame(index = t_train.index)
    merged_train[x_train.columns] = x_train[x_train.columns]
    merged_train[t_train.columns] = t_train[t_train.columns]
    if num == 0:
        total_train = merged_train
    else:
        total_train = pd.concat([total_train,merged_train])

우선 텐서플로우에서 Sequential과 Dense를 import 해줍니다.

그리고 pandas와 numpy도 import 해주세요



그리고 데이터셋을 생성합니다.

데이터셋의 경우 지금까지 만든 데이터들을 모두 다 포함해야 합니다.

이때 주의할 점은 train 데이터이기 때문에, test데이터인 2018년의 데이터를 놔두고

2000~2017년의 데이터를 이용한다는 점입니다.

read_excel을 통해 x와 t데이터를 불러와 주세요.



그 후에 merged_train = pd.DataFrame(index = t_train.index) 라는 코드가 있는데요

여기서 x_train을 쓰시면 안 됩니다.

아까 데이터를 처리할 때 보시면, 실용데이터의 경우 code_data에서 모두 다 쓴 반면

머신러닝 T_data는 한 번 더 nan을 없애주었기 때문에 x_train보다 t_train이 더 적을 수 있기 때문입니다.



그리고 merged_train에 x_data의 컬럼과 t_data의 컬럼 값들을 추가해 준 후

total_train이라는 값에 저장하고, 2017년까지 반복하며 total_train에 추가해줍니다.

 

여기까지 완료하셨으면 total_train에는 이러한 값이 저장되게 됩니다.

이때 index는 중복이 포함될 수 있습니다.

2010년도에 있던 회사가 2011년에 또 존재하면 2년 치의 데이터가 쌓이기 때문입니다.

# 데이터 나누기
x_train = total_train[total_train.columns[:-1]].values
t_train = total_train[total_train.columns[-1]].values

# validation data 생성
x_val = x_train[ int(len(t_train) * 0.9) : ]
t_val = t_train[ int(len(t_train) * 0.9) : ]
    #array로 변경
t_val = t_val.reshape(len(t_val),1)

# train data 재생성 (validation data를 뺀것)
x_train = x_train[ : int(len(t_train) * 0.9) ]
t_train = t_train[: int(len(t_train) * 0.9) ]
    #array로 변경
t_train = t_train.reshape(len(t_train),1)

x_test = pd.read_excel('실용데이터 - 2018.xlsx', index_col = 0)
t_test = pd.read_excel('머신러닝 T_data - 2018.xlsx', index_col = 0)
x_test = x_test.loc[t_test.index].values
t_test = t_test['End'].values.reshape(len(t_test),1)

이 부분은 train, validation, test 데이터를 각각 다 처리해주는 부분입니다.

하나하나 살펴보시면 대부분 아시겠지만, 모르는 부분을 알려드리자면 바로 validation data 입니다.

validation data의 경우 overfitting을 방지하기 위한 데이터라고 보시면 됩니다.

딥러닝을 하게 되면, 아무래도 주어진 데이터에 적응을 하므로,

너무 많이 돌리게 되면, 주어진 데이터에만 반응을 하게 됩니다.

따라서 주어진 데이터 속에서 자체적으로 검증을 해볼 수 있는 데이터셋을 만드는데 그게 바로 validation data입니다.

이렇게 하게 되면 validation data의 loss값이 증가할 때부터 overfitting이 된 것이기 때문에 epochs(딥러닝 반복 횟수)를 조절하기 쉬워집니다.





또 주의하실 점은!! t data 부분입니다. t 값들은 모두 (none,1) 의 값을 가지는 array가 되어야 합니다.

하지만 데이터를 불러오기만 하면 (none,)인 벡터가 되기 때문에 reshape를 통해 array로 만드는 걸 잊으시면 안됩니다.

 

# 모델 만들기
Model_End = Sequential()

Model_End.add( Dense(64, input_dim=len(x_train.T), activation = 'relu') )
Model_End.add( Dense(64, activation = 'relu') )
Model_End.add( Dense(1, activation = 'linear') )

Model_End.compile( loss = 'mse', optimizer = 'rmsprop') # mean_squared_error = mse , optimizer는 Adam과 rmsprop 비교해보기

Model_End.summary()

# 모델 학습시키기
Model_End.fit(x_train, t_train, validation_data = (x_val, t_val) , batch_size = 10, epochs = 100)

이제 모델을  만들고 학습시키는 과정입니다.

Sequential()을 통해서 먼저 모델을 생성해줍니다.



그 후 add를 통해 레이어를 추가해 주시면 됩니다.

이때 은닉층의 activation은 relu로 설정하고 출력층의 activation은 linear로 설정해줍니다

relu는 예측의 정확도가 높다고 하여 주로 사용되는 activation이고

linear는 수치예측을 할 때 사용되는 activation입니다.



첫 두 add 뒤에 있는 레이어의 노드 개수인 64는 아무렇게나 설정해주셔도 되고

마지막 Dense의 출력값만 1로 설정해주시면 수치 하나만 나오게 됩니다.



input_dim의 경우 (50000, 123) 의 array가 있을 때 뒤에 있는 값, 즉 열의 개수를 가져오시면 됩니다.

이유는 가중치인 w와 행렬 곱을 하는걸 상상해보시면 쉽게 이해하실 수 있을 겁니다.



그 후 compile에서 loss는 mse로 정의해주고 optimizer는 rmsprop을 사용해줍니다.

optimizer에서 adam을 사용하는 게 가장 좋다고 알려져 있는 거로 알지만, 수치예측의 경우 rmsprop을 사용하시는 분들이 더 많아 rmsprop을 사용하게 되었습니다.



그리고 summary를 통해 대략적인 모델의 모습을 본 뒤 fit으로 train 시켜 주시면 됩니다.

이때 아까 만든 x_val과 t_val의 경우 validation_data를 이용하여 넣어주시면 됩니다.



batch_size는 한 번에 얼마나 많은 데이터를 검사할 것인가인데, 정확한 개념은 모르겠으나

batch_size가 클수록 딥러닝 속도는 빨라졌고, 오차는 큰 편이었습니다.



epochs는 몇 번을 반복할지 설정해주는 것을 말합니다.

 

loss = Model_End.evaluate(x_test, t_test, batch_size=10)
print('loss : ' + str(loss))

이후에 evaluate를 통해서 모델을 평가해주시면 끝이 납니다.

이렇게 다 하고 나면!! 로스 값이 나오게 됩니다.

그런데 저걸?? 보면... 무슨 생각이 들까요..

내가 잘 한 건가? 하는 생각이 듭니다...

우선 loss 값과 val_loss 값이 전혀 수렴하지 않고 계속 0.8 근방을 맴돌고 있다는 것은 제대로 된 데이터가 없다는 뜻일 것 같았습니다.

그리하여! 이 데이터를 그래프로 나타내 보았습니다.

x축을 실제값, y축을 예측값으로 하는 그래프입니다.

실제값과 예측값이 같을수록 잘 예측한 그래프일 테니.. 당연히 y=x에 수렴할수록 좋겠죠?

그런데 이건 무슨 ㅋㅋㅋㅋㅋㅋ 하나도 예측하지 못한 모습을 보실 수 있습니다.



자 그래서 제가!! 아까 추가했던 4가지 수치에 더불어서 BM이라는 또 다른 수치를 추가하려고 합니다.

 

    #실용데이터 저장 X_data
    practical_data = pd.DataFrame(index = code_data)
    data = total_data.loc[code_data][year]
    pre_data = total_data.loc[code_data][year-1]
    
    practical_data['ROA_Absolute'] = data['당기순이익(원)']/data['총자산(평균)(원)']
    practical_data['Cashflow_Asset'] = data['현금흐름(원)']/data['총자산(평균)(원)']
    practical_data['GPA'] = data['매출총이익(원)'] / data['총자산(평균)(원)']
    practical_data['Profit_increase'] = (data['매출액(원)'] - pre_data['매출액(원)']) / pre_data['매출액(원)']
    #추가
    practical_data['BM'] = data['수정BPS(원)'] / total_price.loc[price_data.index[0]][data.index]

    practical_data[practical_data[practical_data.columns] == float('inf')] = 0
    practical_data = practical_data.fillna(0)
    
    practical_data.to_excel('경로\실용데이터 - %d.xlsx' %year)

BM은 PBR의 역수로, 다른 4가지 지표와는 다른 점이 있습니다.

바로!!! 가격이라는 지표가 포함된다는 것입니다.

다른 녀석들은 단순히 기업이 우량한가, 좋은 수익을 내고 있는가만 보고 있지만

BM은 가격과 함께 기업의 자산을 고려하기 때문에 이를 이용한다면 더 좋은 예측값이 나오지 않을까 하고 이렇게 돌려보게 되었습니다.

새로 추가한 지표를 가지고 다시 실용데이터를 google colab에 넣어주신 다음 모델링을 시켜주세요!!

흠.. 이번에도 역시나 loss와 val_loss가 줄어들지 않고 어느 부분에 머물러 있는데요..

하지만 고무적인 점은 loss 값이 아까보다 줄어들었다는 점입니다!!!

그렇다면 이걸 또 그래프로 한번 봐야겠죠?

왼쪽이 처음 BM없이 예측한 값, 오른쪽이 BM을 넣은 값.

흠.. 별반 차이가 없어 보이지만, 오른쪽을 보시면 살짝 y = x 그래프 쪽으로 기울어진 게 보이실 겁니다.

즉 BM이라는 지표를 넣은 게 어느 정도 효과는 있었다는 말이죠



하지만 이를 통해서 얻은 더 큰 교훈이 있습니다

단순히 재무제표만 이용해서는 컴퓨터도 주가를 예측할 수 없다!! 라는 점이죠

그래서 다음에는 LSTM을 이용하여 주가 데이터를 이용하여 예측해보려고 합니다.



제 마지막 결과물은 인공지능과 일전에 해둔 기초들을 합하여 성과를 내는 것이기 때문에, 앞으로도 포스팅 지켜봐 주시면 감사하겠습니다.!

반응형
Posted by 진우식
,
반응형

안녕하세요. 첫 번째 포스팅에 이어서 두 번째 포스팅을 드디어 쓰게 되었습니다.

오늘 다룰 것은 바로 모든 퀀트 트레이딩의 기초, 과연 데이터는 어떻게 구해야 하는가입니다.



사실 데이터를 구하기는 정말 쉽습니다. 조건이 붙긴 하지만요

' 돈을 지불한다면 '

일반적으로 회사에서 이용하는 데이터들의 경우 정말 많은 돈을 요구합니다.

그래서 개인이 구매하기보다는 회사에서 구매하고, 그걸 이용하여 사람들이 투자에 이용합니다.

따라서 저희처럼 공부하기 위한 사람들은 데이터를 이용하기가 쉽지 않습니다.

저는 그리하여 책에서 배운 방법을 이용하여 데이터를 수집하였고

그 방법을 여러분께 소개해 드리려고 합니다.

 출처 : 퀀트 전략 파이썬으로 세워라 (2019, 박준규, 비제이퍼블릭)

제가 배운 내용은 여러 가지 책에서 차용해온 정보를 통합하여 사용하고 있습니다

하지만 기초 코딩은 우선 학교의 강의와 이 책에서 배웠기 때문에, 저 책을 보시면 상당한 도움이 되시리라 생각합니다.

우선, 제가 사용하는 IDE(통합개발환경)는 스파이더입니다.

파이썬을 사용하실 때 보통 아나콘다를 이용하실 텐데요.

아나콘다를 설치하시면 스파이더가 자동으로 깔리게 됩니다.

보통 쥬피터노트북을 이용하시지만, 저는 이 스파이더가 훨씬 편하게 느껴졌습니다.

다양한 코드들을 색깔을 통해서 눈으로 보기 편하게 해주고

내가 입력했던 변수들을 직접 클릭하여 눈으로 볼 수 있게 해주기 때문입니다.

import pandas as pd
import requests

fs_url = 'http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930&cID=&MenuYn=Y&ReportGB=&NewMenuID=103&stkGb=701'
fs_page = requests.get(fs_url)
fs_tables = pd.read_html(fs_page.text)

 

파이썬에는 다양한 라이브러리 라는 것이 존재합니다.

제가 느낀 라이브러리를 여러분들에게 간단하게 설명해드리자면, 코딩하는 과정에서 계산, 데이터 정리 등등을 편리하게 해주는 것을 말합니다.

크롤링하는 방법은 다양합니다. 그중에서도 대부분 bs4의 BeautifulSoup를 사용하곤 합니다.

하지만 오늘 제가 알려드릴 방법은 조금 더 간편한 방법을 차용하고 있습니다.

바로 requests와 pandas를 이용하는 방법입니다.

requests는 페이지의 정보를 가져오는 역할을 하고, 판다스는 그를 정리해주는 역할을 하고 있습니다

오늘 가져올 데이터는 바로!! 삼성전자의 재무제표입니다.

http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930&cID=&MenuYn=Y&ReportGB=&NewMenuID=103&stkGb=701

 

삼성전자(A005930) | 재무제표 | 기업정보 | Company Guide

삼성전자 005930 | 홈페이지 홈페이지http://www.samsung.com/sec 전화번호 전화번호031-200-1114 | IR 담당자 02)2255-9000 주소 주소경기도 수원시 영통구 삼성로 129 (매탄동) KSE  코스피 전기,전자 코스피 전기,전자 | FICS 휴대폰 및 관련부품 | 12월 결산 PER(Price Earning Ratio)전일자 보통주 수정주가 / 최근 결산 EPS(주당순이익)

comp.fnguide.com

위의 주소로 가시게 되면 아래와 같은 정보를 확인하실 수 있습니다.

이러한 정보들은 포괄손익계산서, 재무상태표, 현금흐름표 3가지로 나뉘어 있습니다.

그리고 별도와 연간으로 나뉘어 있기 때문에 총 3*2 6개의 표가 존재하고 있습니다.

이때 이 6개의 표를 fs_tables에 저장해주는 코드가 바로 위의 코드입니다.

이때 fs_tables는 list의 형태로 저장되게 됩니다.

Python에는 str, int, list, tuple, dict 등 다양한 타입이 존재합니다.

이 타입들에 대해서는 따로 다루지 않을 것이고, 구글링을 통해 검색하시면 바로 나오게 되니 참조하시길 바랍니다.

이때 제가 spyder를 이용하는 이유가 나옵니다.

바로 spyder를 이용하면 이렇게 시각적으로 fs_tables에 적힌 내용을 확인할 수 있기 때문입니다.

variables explorer에 내가 입력한 변수들이 저장되게 되는데요, 그중에서 필요한 내용을 클릭하면 이런 식으로 바로바로 데이터를 보여주게 됩니다.

이때 각 정보는 DataFrame으로 저장되게 되는데요, 이 DataFrame은 판다스의 기본 데이터 타입으로 표 형태로 데이터를 저장해 주는 형식입니다.

그리고 각 데이터의 Value를 더블클릭해 주면

자료1

이런 식으로 눈에 바로바로 보이는 데이터들을 저희에게 전해주게 됩니다.

이러한 데이터들은 list[0]에는 연간 포괄손익계산서, list[1]에는 분기 포괄손익계산서 등이 저장되게 됩니다.

이를 이용하여 내가 필요한 데이터들을 뽑아낼 수 있습니다.

여기서 우리가 필요한 것들 매출액, 영업이익, 당기순이익, 자산, 부채, 자본이라고 가정하고 크롤링을 해보도록 하겠습니다.

매출액, 영업이익, 당기순이익은 포괄손익계산서에 포함된 것들이고

자산, 부채, 자본은 재무상태표에 포함된 것들이라 다른 DataFrame에 위치한다는 것만 알아두시면 되겠습니다.

temp_df = fs_tables[0]
temp_df = temp_df.set_index('IFRS(연결)')
temp_df = temp_df[['2016/12','2017/12','2018/12','2019/12']]
    #temp_df[temp_df.columns[:4]]
temp_df = temp_df.loc[['매출액','영업이익','당기순이익']]

temp_df2 = fs_tables[2]
temp_df2 = temp_df2.set_index('IFRS(연결)')
temp_df2 = temp_df2.loc[['자산','부채','자본']]

fs_df = pd.concat([temp_df, temp_df2])​

fs_tables를 이미 불러왔기 때문에, temp_df에 fs_tables[0]에 담겨있는 정보를 저장해줍니다.

이때 주의할 것은 바로!! index를 설정하는 부분입니다.

DataFrame의 index는 따로 설정해주지 않으면 자료1처럼 0 1 2 처럼 숫자로 되어 있는 걸 보실 수 있습니다.

따라서 IFRS(연결) 이라는 column을 index로 해주기 위해 set_index를 이용해 주었습니다.

자료2

그렇게 하고 나면 이런 자료가 나오게 됩니다.

여기서 우리가 필요한 자료는 2016~2019년의 자료이고, 전년 동기와 전년 동기(%)는 필요 없기 때문에

내가 필요한 데이터들을 뽑아내기 위해 

temp_df = temp_df[['2016/12','2017/12','2018/12','2019/12']]

 

라는 코드를 작성해 주었습니다.

이때 2016/12 2017/12 등등은 제가 작성한 시점에서 만들어진 것이기 때문에 언제나 바뀔 수 있습니다.

따라서

temp_df = temp_df[temp_df.columns[:4]]​

라는 코드를 써주시게 되면 좀 더 일반화하여 데이터를 정리할 수 있습니다.



그 이후에 우리가 포괄손익 계산서에서 필요한 것은 매출액, 영업이익, 당기순이익이기 때문에

loc을 이용하여 행의 값들을 불러와 저장해줍니다.

자료3

그러면 이렇게 필요한 데이터만 추출할 수 있습니다.

마찬가지로 다른 데이터도 정리해주어야 합니다.

자료4

자료4는 fs_tables[2]의 데이터를 가져온 것입니다.

이때도 역시 인덱스를 재설정해 주어야 하는 것은 맞지만, 포괄손익계산서와는 다르게 전년 동기 자료가 없습니다.

따라서 그 부분을 빼고서 자산, 부채, 자본을 불러와 줍니다.

자료5

그러면 이렇게 temp_df와 temp_df2 각각 저장이 되게 됩니다.

아까 자료3에 있던 temp_df와 temp_df2를 합쳐서 보면 더 깔끔하게 자료를 볼 수 있기 때문에

이 데이터들을 하나로 합쳐주기 위해 마지막에 있는 pd.concat을 활용하였습니다.

fs_df = pd.concat([temp_df, temp_df2])

 

이는 같은 columns(표에서 열에 해당하는 자료)를 공유하는 데이터들을 합쳐주는 역할을 합니다.

따라서 temp_df와 temp_df2는 2016/12, 2017/12, 2018/12, 2019/12라는 columns들을 공유하고 있기 때문에 두 개를 합치게 되면

자료6

이렇게 합쳐진 데이터가 나오게 됩니다.

이를 활용하여 나중에 회사를 평가하는데 사용할 예정인데요.

오늘은 너무 많은 내용을 공부한 것 같아 2탄으로 나머지 내용은 넘기려고 합니다.



퀀트 트레이딩이라는 것은 하나의 회사가 아니라 모든 회사를 다 한 번에 평가하기 위한 것으로

사람의 머리로 할 수 없는 걸 하기 위해 하는 것이겠죠?

빅데이터를 구해야 하는데 이 정도로는 빅데이터라고 할 수 없으니

다음 시간에는 삼성전자뿐만이 아니라 모든 회사의 정보를 한 번에 정리할 방법을 알려드리도록 하겠습니다.

반응형
Posted by 진우식
,
반응형

현재 글을 쓰는 시점 완성한 공식을 통해, 파이썬으로 가상투자(백테스팅)를 한 결과, 2004년 5월 ~ 2019년 4월 까지 최대 35배의 수익을 창출하는 공식을 만들어냄

안녕하세요 지누입니다.

우선 첫 번째 포스팅이기 때문에 간단한 제 소개부터 하고 넘어가려고 합니다!

소개가 궁금하지 않으신 분들은 다음 포스팅부터 봐주시면 감사하겠습니다~



제가 블로그를 시작하는 건 벌써 2번째인데요,

사실 첫 번째 블로그는 상업적 용도로 많이 사용하였습니다.

학창 시절이었기 때문에 식사한다거나, 여행을 가거나, 각종 전자제품 등등 필요한 것이 있을 때

블로그를 이용하면 싸게 이용할 수 있어서 좋았었죠.



하지만 그러다 보니 블로그의 방향성이 처음에는 솔직함과 제 일기를 써 내려 가다가

나중에는 단순히 가게를 소개하는 수준에 그치고, 업체들을 신경을 쓰다 보니 솔직한 말을 전하기가 힘들게 되었습니다.

그래서 제 솔직한 이야기들을 쓰기 위해 이렇게 다시 블로그를 하나 개설하게 되었습니다.



그중에서도 제가 가장 관심이 있는 분야가 있습니다.

바로 퀀트 트레이딩입니다.

 

우선 저는 남들이랑은 조금 다른, 특이한 이력이 있습니다.

문과임에도 불구하고 수학을 정말 좋아하며

초6부터 간단한 매크로이긴 하지만, 프로그래밍하기 위해 오토핫키를 손대기 시작하였고

중2 때는 간단하지만 비쥬얼 베이직이라는 것도 접하게 되었습니다.

https://cafe.naver.com/nrdr/72445

지금은 카페가 바뀌었지만

예전에 제가 즐기던 나랜디라는 게임이 있었습니다.

상당히 조합식이 까다로워서 초보자가 접하기엔 너무나 어려운 게임이었고

그걸 다 외우기에는 고등학생이었던 저는 게임을 light하게만 즐기고 싶었기에

이렇게 유닛의 숫자만 입력하면 어떤 유닛이 조합 가능한지 알려주는 프로그램을 만들기도 했습니다.



정말 간단한 프로그래밍이어서 지금 와서 코드를 공개하면... 사실 조금 쪽팔리기도 하겠지만

언젠가는 공개할 예정입니다.

2015년 2월에 만들었던 이 녀석을, 이용자들의 요구사항에 따라 점점 수정해나가면서

오류수정과 기능추가를 반복했습니다.

고3인 시절이었기 때문에 잦은 업데이트는 하지 못했고

수능이 끝난 후 11월 이후 좀 더 많은 기능을 한 번에 업데이트하였고

많은 사람이 이용해줌에 쾌감을 느끼게 되었습니다.



문과로 대학을 왔음에도 불구하고, 수학에 굉장히 관심이 많고

어릴 때부터 코딩을 해왔던 지라 사실 문과적 성향과 잘 맞지는 않았습니다.

그래서 경영학과에서 그나마 이과와 관련이 많은 회계와 재무 쪽으로 관심이 가게 되었습니다.



처음에는 회계 쪽으로 나아갈까 했지만, 고등학생 때부터 주식에 관심이 많았기 때문에

주식을 좀 더 전문적으로 공부하고 싶어졌습니다.

그러던 와중 교수님을 통해 퀀트 트레이딩이라는 분야를 접하게 되었고

현재는 여러 가지 공식들을 발굴해내는 과정에 있습니다.



퀀트 트레이딩에 있어서 공식을 발굴해낸다는 것은 개발자의 생명과도 같은 것이기 때문에

github에 따로 공식을 공개하지 않고

제가 공부를 해왔던 방식들을 여러분께 소개해드리려고 합니다.

완전한 정보는 아니지만, 저의 공부방식과 더불어

중간중간에 제가 이용한 데이터들에 대한 간단한 정보 정도는 얻으실 수 있을 것입니다.



제가 원하는 최종 목표는 인공지능을 이용해서 딥러닝을 시켜, 자동매매를 하게 만드는 것이고

지금은 그 과정에 있어서 인공지능 없이, 제가 공식을 하나씩 수정해나가며 가장 효율적인 방법을 찾고 있습니다.

수학, 코딩 그리고 경영학적 지식을 모두 다 요구하는 퀀트 트레이딩

제가 이때까지 공부해왔던 모든 것들을 망라할 수 있는 가장 좋은 방법이라고 생각합니다.

자주 포스팅하지는 않겠지만, 여러분께 도움이 되는 포스팅이 되었으면 합니다.



다음 포스팅은 우선 가장 중요한 데이터를 크롤링하는 방법을 여러분께 소개해드리려고 합니다.

퀀트 트레이딩에 있어서 데이터가 없이는 아무것도 이루어질 수 없습니다.

과거의 데이터를 통해 미래를 예측하는 것이기 때문에, 첫 스타트로서 가장 좋은 포스팅이라고 생각됩니다.



제 포스팅의 중점은 단순히 코딩에만 있지 않습니다.

코딩과 더불어서 이 데이터가 왜 중요한지, 어떻게 써야 하는지 하나하나 알려줄 것이기 때문에

경영학과, 컴퓨터공학과 두 학과를 전공하신 분이 아니시라면 이해하기가 약간 어려울 수도 있지만

최대한 이해가 될 수 있도록 노력하겠습니다.

반응형
Posted by 진우식
,