맨위로버튼이미지

주희 씨는 어느덧 인생의 중년을 맞았습니다. 남편은 대학교수를 하고 있고 큰 아들은 지방 유명대에서 대학원을 다니고 있습니다. 둘째 아들은 대학 2학년을 휴학하고 군대에 갔습니다. 남편은 공부가 취미인 사람이라 큰 집에 홀로 남겨진 것 같다는 생각을 매일 같이 합니다. 공부를 쓱 잘하지는 못했던 주희 씨를 마음 하나만 보고 선택해준 남편을 옛날에는 잘 생긴 교회 오빠로만 생각했지만 남편은 그렇지 않았나 봅니다. 친구들에 끌려 교회에 처음 온 주희 씨에게 첫눈에 큰 콩깍지 붙어 버렸습니다. 말 수가 적던 남편은 유학을 가기 전 은행 창구에서 일을 하던 고졸 행원이었던 주희 씨를 찾아갔습니다. 집에서 버스로 10 정거장을 가는 거리지만 30분이 넘는 시간이 금방 지나갔습니다. 그때는 은행 마감시간이 지금처럼 3시가 아니었습니다. 해가 불그스레할 때쯤 은행 쪽문으로 들어가 두장의 입금표에 십만 원을 입금하는 것과 다른 한 장에는 밖에서 기다리겠다는 글을 적고서 능청스럽게 주희 씨가 있는 창구로 가서 손을 내밀었습니다. 주희 씨가 고개를 숙이고 일을 하다 입금자의 이름을 보고 깜짝 놀라 얼굴을 빼꼼히 올립니다. "오빠아?" 놀란 주희 씨가 계속 눈을 깜박입니다. 그 은행의 통장이 없던 남편을 위해 얼른 새 통장을 꺼내고 남편의 도장을 찍어서 스티커를 정성스레 붙이고는 뒷줄에 차장님께 결재서류를 맞기고 얼른 돌아와 돈을 손으로 한번 계수기로 한번 센 다음에서야 두 번째 입금표를 보았습니다. 그 입금표는 남편에게 돌려주면서 마치려면 한참 걸릴 거라고 은행 앞 분식집에서 기다리라고 말했습니다. 남편은 새로 발급된 통장을 받고서는 능청스레 아무 일도 없다는 듯 조용히 은행 쪽문을 빠져나옵니다. 그때서야 남편의 뒷 모습이 눈에 들어옵니다. 오늘은 평소에 입지 않던 정장 차림에 약간 멋도 부린 모습입니다. 사실 은행 마감시간은 손님을 더 받지 않는 시간이고 행원은 그 뒷 시간이 더 바빴습니다. 오늘 들어온 돈가 나간 돈을 다 맞추어서 딱 맞는지 매일 확인을 합니다. 옆 창구 대리님이 벌써부터 눈치를 채고 전표를 맞추는 주희 씨에게 남은 건 자기가 할 테니 빨리 가보라 합니다. 덕분에 은행에서 직원들은 말은 하지 않았지만 주희 씨의 사정을 다 알아 버렸습니다. 그렇지만 누구 하나 주희 씨에게 부담을 주지 않으려 아무도 말을 하지 않았습니다. 주희 씨는 더 열심히 일을 하고 있었습니다. 그러자 뒷줄에 차장님이 일이 끝난 사람에게 주희 씨 일을 좀 나눠서 하라고 하고 주희 씨를 얼른 내보냅니다. 그리고 큰소리로 "오늘은 배가 고픈 사람도 요기 앞 분식집은 가지 말고 얼른 집으로 갈 것. 알겠지들"말하며 뒤로 돌아 자리로 돌아간 뒤 고개도 들지 않고 서류를 넘깁니다. 주희 씨는 하는 수 없이 직원들에게 미안하다는 말을 수 없이 하며 종종 걸음으로 분식집을 향했습니다. 분식집에는 한참을 기다린 남편의 모습이 보였습니다. 원래도 잘 생긴 얼굴로 남들에게 위화감을 주던 얼굴이 저녁 햇살에 노을빛을 받아 더 잘 생겨 보였습니다. 그렇게 외국으로 유학을 가는 남편을 2년을 더 기다리게 되었습니다. 한국으로 돌아온 남편은 모교의 조교수가 되었고 둘은 결혼을 했습니다. 사실 주희 씨는 교회를 형식적으로 다녔지 신앙이 있지는 않았습니다. 그러고 집에 부모님도 교회와는 거리가 멀었습니다. 그렇지만 시댁 어른들은 남편의 말이라면 팥으로 메주를 쑨다고 해도 믿었습니다. 사실 시댁 어른들도 착하고 이쁜 주희 씨가 처음 집에 왔을 때 모두 반해 버렸습니다. 학력이 고졸인 건 순전히 집안 형편으로 대학을 못 가고 바로 취업했기 때문이라고 생각 했습니다. 특히 시어머니는 늘 어디를 가던 며느리 자랑입니다. 시어머니는 교회의 아침 기도를 꼬박꼬박 다니시면서 주희 씨가 일어나기 도전에 아침밥상이 다 차려져 있었습니다. 첫 아이를 가지고 주희 씨는 6여 년을 다니던 은행을 그만두었습니다. 시어머니의 보살핌이 너무나도 극진해서 친정엄마와 시어머니 중 누가 진짜 엄마인지 헷갈릴 정도였습니다. 그런 환경에서 살아서 그런지 주희 씨도 사랑이 넘치는 사람이었습니다. 애들이 조금 자라서 손이 들 가게 되자 주희 씨는 주말마다 봉사를 나갔습니다. 그곳은 부모가 있지만 피치 못한 사정으로 시설에 맡겨지는 그런 아이들이 있는 곳입니다. 하루 동안 그 애들의 대리 엄마가 되어서 밥도 하고 빨래도 하고 공부도 봐주고 하루를 바쁘게 쓰고 옵니다. 남편가 함께 출가하고 시댁 어른들과 떨어져 살게 되고 애들도 다 커니 갑자기 주희 씨에게 마음의 병이 찾아왔습니다. 소히 말하는 빈둥지 증후군인가 봅니다. 시설에서 위탁가정을 찾는 다는 것을 알게 되었습니다. 신청부터 하고 남편에게 상의를 했습니다. 이를 수 있는 건 남편의 착한 성격을 이미 알고 있기 때문이었죠. 주희 씨집으로 오게 되는 아이는 지연이 였습니다. 엄마가 얼마 전 교통사고로 돌아가시고 돌봐 줄 친척도 없어 시설에 입소했지만 무슨 일인지 모르는 일로 아버지가 교도소에 계셔서 다른 곳으로 입양을 갈 수는 없었습니다. 그날은 남편도 일찍 퇴근해서 주희 씨를 도와주었습니다. 주희 씨는 먼저 지연이를 맞이 했습니다. 목욕도 직접 시켜 주고 새로 싼 옷도 입혀 주었습니다. 지연이는 말수도 적고 널 먼 곳만 보고 있지만 착한 아이라는 건 하는 행동들을 보면 금방 알 수 있었습니다. 저녁 상은 주희 씨의 실력이 총 출동했지만 지연이는 밥을 많이 먹지는 않네요. 다음 날이 되었습니다. 지연이는 주희 씨의 집에 하루를 보낸 첫날이었습니다. 아침을 빠르게 먹고는 집 근처 초등학교를 갔습니다. 지연이는 6학년입니다. 교무실로 가서 전학 신고를 하고 담임으로 배정받은 선생님을 따라 교실로 갔습니다. 주희 씨는 교실로 따라 들어가지는 않고 밖에서 지연이가 자기 자리에 앉을 때까지 기다렸습니다. 한참을 보다 지연이와 눈이 마주치자 손인사를 마치 소녀처럼 마구 흔들고 또 한참을 보고 집으로 돌아왔습니다. 학교를 마치는 시간이 되자 주희 씨는 눈썹을 휘날리며 쏜살같이 학교 교문 앞으로 갔습니다. 어깨가 축 쳐진 지연이를 보았습니다. 아직 낮 쓴 환경에 적응을 못한 모습이네요. 주희 씨는 두 아들을 명문대에 보낸 실력을 발휘해 지연이의 공부를 봐주었습니다. 지연은 아직 공부를 잘하지는 못하는 듯 보였습니다. 그래도 주희 씨는 칭찬을 아끼지 않았습니다. 아무리 사랑을 모르는 사람이 봐도 주희 씨의 착한 마음은 예나 지금이나 사랑이 넘치고 에너지가 넘쳤습니다. 그리고 그 기운은 금방 상대를 변화시킵니다. 얼마 전 잠시 찾아왔던 마음의 병은 이미 도망간 지 오래였습니다. 며칠 후 둘째 녀석이 휴가를 나왔습니다. 아빠를 닮아 안 그래도 잘생긴 둘째는 구릿빛 얼굴에 짧게 자른 머리로도 인물을 가릴 수 없습니다. 아빠보다 훨씬 큰 키에 몸에 잔근육까지 붙자 더 잘생겨졌습니다. 아님 주희 씨도 예전의 남편처럼 큰 콩깍지가 덮였는지도 모르겠네요. 지연은 부끄러움이 많아서 그런지 얼굴을 들고 아들을 보지는 않았습니다. 저녁식사가 거하게 체려지고 남편은 오랜만에 아들에게 바둑 시합을 요청합니다. 주희 씨는 남편이 좋아는 생막걸리에 맛있는 안주를 내옵니다. 오랜만에 온 가족이 시끌벅적합니다. 지연은 말없이 조용히 자기 방으로 들어갑니다. 주희 씨는 둘째에게 지연이가 네 방을 쓰고 있으니 오늘은 형방에서 자라고 일러 줍니다. 아들과 남편의 대국은 몇 판이 더 되고서 끝이 납니다. 지연의 방은 벌써부터 불이 끄져 있어서 주희 씨는 뒷정리를 하고 모두 그렇게 즐거운 저녁이 끝이 납니다. 다음 날 아들과 함께 옛날 장터에 저녁장을 보러 갔습니다. 지연이 학교를 마치는 시간과 쌀짝 어긋나 지연은 데려오지 못했습니다. 벌써 해는 뉘엿뉘엿 곱게 노을을 만들고 있었습니다. 두 사람은 서둘러 집으로 향했습니다. 주희 씨는 지연이와 둘째에게 줄 음식 생각에 벌써 기분이 좋아집니다. 집안이 이상하게 보인 것은 그때였습니다. 지연의 흔적이 없었습니다. 남편에게 급히 전화를 하고 근처 파출소로 향했습니다. 새로운 환경에 어디 갈 곳도 없을 지연을 생각하자 마음이 급해졌습니다. 담임선생님에게 급히 연락을 하고 혹시나 친구가 생겼는지 물었습니다. 담임선생님도 여기 저기 전화를 해보지만 딱히 지연이 갈만한 곳은 없었습니다. 둘째는 동네의 PC방과 만화가게 등 어린 애들이 갈 수 있는 곳은 전부 찾아보았습니다. 벌써 날은 어두워서 앞이 잘 보이지 않았습니다. 동네 어르신들이 올라가 새벽 운동을 하시는 작은 동산 앞에 놀이터가 있어서 그기를 가보았습니다. 날은 벌써 예전에 어두워진 놀이터 그네에 누군가 있습니다. "지연이니?" 둘째가 물었습니다. 휴대폰 불빛으로 얼굴을 비춰보니 어제 처음 본 얼굴이지만 예쁜 지연이 얼굴을 둘째는 금방 알아볼 수 있었습니다. 손을 잡아보니 얼음장입니다. 주머니에 향상 있는 장갑을 끄네 씨워 주기 전에 최대한 손을 잡아 녹여 주었습니다. 그리고는 장갑을 씨워 주고 친구가 일하는 편의점으로 갔습니다. 친구가 반갑게 맞아 줍니다. "오 이 예쁜 손님은 누구니?" 친구의 장난 썩인 말에 "내 동생이다. 껄떡이지 마라" 친구는 말없이 덮혀진 뜨거운 커피캔 하나를 꺼내서 지연에게 주었습니다. 이때까지 한 번도 얼굴을 들지 않아서 보지 못했던 둘째 오빠와 그친구의 얼굴을 보게 되었습니다. 두 조각 미남이 이었습니다. 지연은 처음으로 얼굴이 빨개 졌지만 추위 때문인지 무엇 때문인지는 알 수 없습니다. 머리 속에 크고 맑은 종소리가 들렸습니다. 심장은 미친 듯이 꽁닥이고 있었습니다. 둘째는 커피값을 계산하고 걱정하고 계실 어머니에게 전화를 했습니다. 주희 씨는 그때서야 안심을 하는 모양입니다. 지연은 낮에 일이 비로써 기억이 났습니다. 학교를 마치고 나오는 길에 누군가를 보았습니다. 분명 아버지였습니다. 지연의 집은 그리 좋은 환경은 아니었습니다. 아버지가 술을 먹고 오는 날은 방문을 잠거고 귀를 막았습니다. 엄마의 비명이 들려도 절대 밖으로 나가면 안 되었습니다. 아버지가 하던 일이 잘못되어 교도소에 가게 되자 엄마는 급히 월세를 빼서 집을 옮겼습니다. 엄마는 할 수 있는 모든 일을 다하셨습니다. 식당의 주방 설거지와 같은 허드레 한일도 마다하지 않았습니다. 저녁 늦게 돌아오시는 길에 사고가 났습니다. 병원에 갔을 때는 이미 어머니는 돌아가셨습니다. 고아였던 어머니는 가족이 없었고 도와줄 친척도 없어서 지연은 시설로 갔었습니다. 지연이에게는 아버지는 공포 그 자체였습니다. 집에 주희 씨가 없다는 것을 이미 알고 있었습니다. 도망치듯 미친 듯이 달렸습니다. 한참이 지나 저녁이 되고서야 문제가 생겼습니다. 지연에게는 아직 전화가 없다는 것과 이 동네는 아직 낮 쓸다는 것이었습니다. 그렇게 한참을 배회하던 지연은 작은 놀이터와 만났습니다. 어둠이 오히려 그날의 공포를 덮허 주었습니다. 한참 후 낯익은 목소리를 듣고서는 온몸의 힘이 다 빠져나가는 것을 느꼈습니다. 둘째는 친구가 아르바이트를 하고 있는 통신사 대리점을 갔습니다. 초등학생이 사용할 수 있는 중급 모델의 핸드폰이랑 액세사리를 쌌습니다. 그리고는 엄마 주희 씨 번호와 아버지 번호 그리고 자기 번호를 저장해서 지연에게 핸드폰을 주었습니다. 그리고는 준 전화로 자기에 전화를 걸어서 번호를 받은 뒤 내 동생이라고 번호를 저장했습니다. 그렇게 집으로 돌아와 주희 씨는 사정을 알게 되었고 다음날 지역의 큰 경찰서에 가서 자세한 사정 이야기를 했습니다. 청소년과 담당 형사과장을 뵈었을때는 이미 주위 기동대에 사정 이야기를 해두었다는 말을 들었습니다. 지연이 아버지는 아직 보호 처분 중이라 보호관을 통해서 연락이 갔다고 합니다. 며칠 후 둘째는 휴가를 마치고 복귀를 했고 주희 씨는 혹시라도 무슨일이 생기까 몇 달간을 지연이 마중을 가고 등교를 챙겼습니다. 그리고 겨울이 지나 지연이는 어엿한 중학생이 되었습니다. 그리고 지연에게 변화가 생겼습니다. 둘째 오빠에게 매일 문자를 한다는 겁니다. 그리고 공부도 더 적극적이 되었습니다. 역시 아들 둘을 명문대에 보낸 주희 씨의 실력은 줄지 않았습니다. 지연은 매번 반에서 일등을 놓친 적이 없었습니다. 전역한 둘째는 서울에 있는 대학교로 돌아갔지만 방학 때면 어김없이 내려와 지연의 공부를 봐주었습니다. 지연은 생각했습니다. 이런 조각 미남이 가르쳐 주는 공부가 머리에 안밖히면 도대체 어디로 가겠냐고. 그러나 한 동안 행복했던 가족에게 올 것 왔습니다. 지연의 아버지가 찾아왔습니다. 남편이 지연의 아버지를 집에서 조금 떨어진 카페로 데려갔습니다. 그간 이야기를 둘은 서로서로 했습니다. 남편은 지연 아버지에게 지연의 트라우마와 어머니의 사망 후 시설로 갔었던 이야기, 그리고 위탁가정으로 우리 집으로 온 이야기를 해주었고 현재 지연의 상태도 이야기해주었습니다. 지연의 아버지는 한 동안 아무 말도 없이 있었습니다. 그렇게 노을이 붉은 해를 삼킬 때쯤 지연의 아버지는 다른 나라로 일을 가게 되었다며 몇 년 동안은 지연을 보기 힘들 거 같다는 말과 지연이 맑게 자랄 수 있게 해 준 것에 대하여 남편에 감사를 표하고는 말없이 사라졌습니다. 그리고 얼마 후 법원으로부터 지연의 아버지가 친권을 포기했다는 내용의 통지문이 왔습니다. 남편이 지연의 아버지에게 권했던 것입니다. 남편은 빠르게 지연의 입양 절차를 밟았습니다. 그렇게 지연이 고등학교에 들어가는 날 지방에 있던 큰 애와 서울에 둘째까지 전가족이 모인 자리에서 생일잔치가 열렸습니다. 오늘은 지연과 주희 씨가 위탁모와 보호소녀가 아닌 진짜 엄마와 딸로 만나는 날입니다. 모든 가족은 가족으로써 첫 생일을 맞는 지연을 축하했습니다. 그렇지만 지연이는 사랑하는 사람을 떠나보내야 했습니다. 4년 동안 짝사랑하던 사람을 이젠 진짜 오빠로 불러야 하니 말입니다. 그렇지만 이 잘 생기고 멋진 오빠를 평생 볼 수 있는 것은 변함이 없었습니다. 오빠가 따뜻하게 잡아 주던 그 손을 잊을 수가 없었습니다. 그 손에 따뜻함으로 주희 씨의 사랑을 배울 수 있었습니다. 지연은 주희씨에게서 처음으로 엄마의 소중함과 사랑의 힘의 위대함을 느꼈습니다. 밤하늘을 두 사람의 사랑처럼 고운 흰 눈이 덮어 주고 있었습니다.

맺은 말
가정 폭력은 여러 사람의 정신을 죽이는 범죄입니다. 하지만 우리나라의 많은 법은 그런 가정폭력을 해결해 주지 못하고 있습니다. 우리는 '원가정 원칙'이라는 법원칙을 가지고 있습니다. 가족 간 성범죄나 가정폭력으로 큰 상처를 받은 청소년을 범죄현장이었던 다시 그 가정으로 보내어지고 있습니다. 우리는 얼마나 많은 희생을 보고 나서 그런 법들을 지워나갈 수 있을까요? 가정폭력을 막기 위한 대처보다 피해자 구제가 더 시급해 보이는 것은 저만의 생각일까요? 법은 처벌의 중요성보다 피해자 구제를 위한 국가의 행위에 대한 정의가 더 필요해 보입니다.

반응형
LIST

'일상의 생각 > 자작 소설' 카테고리의 다른 글

다른듯 같은것(1)  (0) 2022.12.19
삶 그리고 가족  (1) 2022.12.17
코마(끝나지 않는 기억)  (0) 2022.12.13
가을로 겨울로  (0) 2022.12.02
식구를 늘리는 냥이  (0) 2022.11.18

맨위로버튼이미지

영화 아담스 패밀리의 장녀 웬스데이가 어머니와 아버지가 다닌 기숙학교에서 벌어지는 살인사건을 해결하는 탐정 드라마 이다. 팀 버튼 감독은 영화에서의 기본설정을 비꼬는 내용을 넣으면서 인물들을 더 다차원적으로 보이게 한다. 괴물이 여럿 나오는데 한가지 방법이 아니 여러가지 특수 효과를 사용하고 특히나 범인으로 나오는 괴물의 경우 감독 늘 사용하던 캐릭터 방식의 3차원 애니로 처리한다. 사실 눈치만 좋으면 감독의 숨겨진 장치들을 다 찾을 수 있다.학교라는 장소 설정으로 다채로운 캐릭터(각 문파의 고수라고 해야 하나...)가 출연 가능하다. 처음에 의심 받던 친구나 동료가 하나 둘씩 죽거나 아니면 각성해서 강력한 조력자가 되기도 한다. 처음부터 웬즈데이는 소설을 쓰고 있고 물론 그내용은 이 사건이다. 극에서 필요가 없는 캐릭터는 사용 후 바로 패기처리된다. 처음에는 하루에 한 편식 천천히 보려고 했지만 어느새 마지막 회를 달리고 있었다. 모두가 범인같고 모두가 범인이 아닌 듯 하다가 다시 의심받고 그렇게 영화는 끝을 향해 간다. 한가지 궁금한 점은 미국은 정말로 학교마다 뭔가 친구 이벤트를 하는 지 중간에 3팀의 경기 장면이 나온다. 기발하지만 영화에 필요한 장면인지는 잘 모르겠다. 거의 마지막에서야 진범을 잡지만 그 사이 많은 희생이 따른다. 그 중에서 아주 중요한 인물이 자기 종족의 특징으로 범인을 잡는 결정적인 역활을 하지만 최후를 맞는다. 사실 가장 의심을 많이 받게 감독은 필요하지 않는 여러 장면을 만들어 넣어 주인공으로부터 의심을 싸게 만든다. 역설적으로 나 범인 아님 이라고 말하는 것처럼 . 설정은 별종(벰파이어,메두샤,하이드 등)과 사람이 공존하는 듯 하지만 끝으로 가면 거의 대부분이 별종이나 별종 가족이다. 오히려 아담스 가족처럼 별종이라고 밝히고 사는 쪽이 오해와 혐오를 받지만 나중에는 누가 누굴 혐오한 건지. 알 수가 없다. 모든 인물은 웬즈데이의 색깔 알러지 교복 처럼 모두 회색으로 변하고 그리고 나서 서로 용서하고 화합한다. 감독의 의도가 아닐가? 서로 의심하던 사이의 인물들은 친구나 연인이 되고 그 사이에 웬스데이도 변하게 되고 성장한다. 학교는 많은 희생자로 인해 강의를 취소하고 웬즈데이는 집으로 돌아가지만 학기가 다시 시작되고 학교로 돌아 오게 된다면 시즌2는 시작할 것이다 시리즈는 다행히 세계 1위를 하면서 가능성을 만들었다. 넷플릭스는 드라마의 생태계도 바꿔 놓았다 팀버튼 감독 특유의 초반 도입이 길어 지거나 중반이후 루즈해지는 부분이 전혀 없어지고 단백한 맛만 볼 수 있다.넷플릭스의 노력이 돋 보인다.

 

 

반응형
LIST

'영화이야기' 카테고리의 다른 글

트랜센던스(노 스포)  (1) 2022.12.22
헌트 리뷰(누가 범인) 노 스포  (0) 2022.12.17
커넥트(신인류의 시작) 리뷰  (0) 2022.12.08
올빼미 리뷰  (0) 2022.11.28
완다비젼 리뷰(노 스포)  (0) 2022.01.12

맨위로버튼이미지

네이버 웹툰 원작을 보지는 않아 어느 쪽이 우위인지는 살짝 헷갈리지만 전반적인 톤은 스위트 홈을 따르고 있다 심지어 특수효과의 대부분도 스위트 홈의 축소판 정도이고 제목이나 스크립트도 마찬가지다. 아무리 일본의 최고 감독이라 해도 우리나라 제작진에 비해 퀄리티가 많이 떨어진다. 가장 심한건 서사의 전개이다. 시즌1은 시리즈의 1편 느낌이다. 딱 그정도다. 서사는 최악이다. 마치 엄청난 재료로 이것 밖에 라는 느낌이다. 스릴감도 많이 떨어진다. 뭉쳐있는 실타래의 느낌은 어디서도 찾아 볼 수 없다. 안타까운 느낌마저 든다. 최상급 출연진의 연기는 무너진 서사로 애들 장난같이 보인다. 특히 그 연기 잘하고 인물까지 잘 생긴 정해인 배우의 연기는 안습이다. 배우의 잘못이 아니라 감독의 잘못이다. 감독이 한 인터뷰 그대로다. 일본의 제작기술이나 영화산업 전반은 이미 오래전에 무너졌다. 3류급 카메라 기술로 담기에는 영화의 내용이 너무 어렵다. 아이디어만은 최고라고 인정하고 싶다. 그렇지만 영화는 1차원적 서사를 벋어나지 못한다. 시즌2를 더 기다리게 만드는 역활로써 확실했지만 시청률이 나오지 않으면 이 재밌는 이야기는 묻칠 것이다 넷플릭스에는 시즌1의 영광을 이어가지 못한 시리즈가 한 트럭은 될것이다. 부디 그런일이 없게 스튜디오 드래곤의 역활이지 않을까? 참 스위트 홈도 이 집 작품아닌가? 시즌2로 가지는 못한것 같은데 음....

 

 

반응형
LIST

'영화이야기' 카테고리의 다른 글

트랜센던스(노 스포)  (1) 2022.12.22
헌트 리뷰(누가 범인) 노 스포  (0) 2022.12.17
웬스데이 리뷰(노 스포)  (1) 2022.12.09
올빼미 리뷰  (0) 2022.11.28
완다비젼 리뷰(노 스포)  (0) 2022.01.12

맨위로버튼이미지

import random
from collections import deque, namedtuple
from typing import Dict, List, NamedTuple, Optional, Text, Tuple, Union
# from IPython.display import display, Math
from Account import Account
import math
from itertools import count
import os

import sys
import os.path as path
import numpy as np
import plotly as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as T
from Market import Market
from PIL import Image
from plotly import express as px
import torchvision
import time
import joblib
import pandas as pd
from dqn import DQN
from memory import ReplayMemory, Experience

action_kind = 21
max_episode = 5000
screen_height = 100
screen_width  = 140
data_size = 600
epsilon = 0.3
dis = 0.9

BATCH_SIZE = 8
# GAMMA = 0.999
GAMMA = 0.99
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any
WINDOW_START = 0
WINDOW_SIZE  = 1500

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))

# if gpu is to be used
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# device = torch.device("cuda")
# device_str = "cpu"
device_str = "cuda"
device = torch.device(device_str)
    
memory = ReplayMemory(BATCH_SIZE)
converter = torchvision.transforms.ToTensor()
market = Market()
train_net = DQN(device, screen_height, screen_width, action_kind, 32).to(device)
train_net = nn.DataParallel(train_net, device_ids=[0,1]).to(device)

episode_durations = []
    
optimizer = optim.RMSprop(train_net.parameters(), lr=0.000001)

# def select_action(df, idx):
#     try:
#         global steps_done
#         sample = random.random()
#         eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
#         steps_done += 1
#         clslvl = ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * 40 // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
#         if sample > eps_threshold:
#             if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
#                 action = clslvl
#                 return  action
#             elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
#                 action = clslvl
#                 return  action
#             else:
#                 return clslvl
#         else:
#             action = random.randrange(action_kind)
#             return  action
#     except Exception as ex:
#         exc_type, exc_obj, exc_tb = sys.exc_info()
#         fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
#         print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
#         return 0

def select_action(df, idx):
    return ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * (action_kind-1) // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
        
def get_chart(market, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    # if  idx > (df.index.max() - data_size - 1):
    #     return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # display(img)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    chart = converter(img).unsqueeze(0)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
    # display.clear_output(wait=True)
    # display.display(plt.gcf())
            
def main():
    if path.exists("pt/train_net_{}.pt".format(device_str)):
        train_net.load_state_dict(torch.load("pt/train_net_{}.pt".format(device_str)))
        
    for _ in range(10):    
        df = market.get_data()
        # df = df.head(WINDOW_SIZE)

        for epoch in range(max_episode):
            account = Account(df, 50000000)
            account.reset()

            last_chart = get_chart(market, data_size, data_size)
            account.reset()

            for idx,_ in enumerate(df.index, start=(data_size + 1)):
                try:
                    since = time.time()
                    curr_chart = get_chart(market, idx, data_size)
                    if curr_chart is None or last_chart is None:
                        continue
                    else:
                        state = curr_chart - last_chart
                        
                    last_chart = curr_chart
                    reward = 0
                    num_action = select_action(df, idx)
                    reward, real_action = account.exec_action(2 if num_action == (action_kind - 1) else (1 if num_action == 0 else 0) , idx)
                    print("idx:%d==>action:%d,real_action==>%d price:%.2f"%(idx, num_action, real_action, df.loc[idx, 'close']))
                    # reward = torch.tensor([reward], device=device)
                    num_action = torch.tensor([[num_action]], device=device, dtype=torch.int64)
                                    
                    memory.push(curr_chart, num_action, reward)
                    if len(memory) >= BATCH_SIZE:
                        epsode = memory.pop(BATCH_SIZE)
                        batch = Experience(*zip(*epsode))
                        # non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,
                        #                                       batch.next_state)), device=device, dtype=torch.bool)
                        # non_final_next_states = torch.cat([s for s in batch.next_state
                        #                                             if s is not None])
                        state_batch = torch.cat(batch.state)
                        action_batch = torch.cat(batch.action)
                        # reward_batch = torch.cat(batch.reward)
                        train_net.train()
                        state_action_values = train_net(state_batch).gather(1, action_batch)
                        # next_state_values = torch.zeros(BATCH_SIZE, device=device)
                        # next_state_values[non_final_mask] = train_net(non_final_next_states).max(1)[0].detach()
                        # expected_state_action_values = (next_state_values * GAMMA) + reward_batch
                        # print(action_batch)
                        # print(state_action_values)
                        optimizer.zero_grad()
                        criterion = nn.SmoothL1Loss()
                        loss = criterion(state_action_values, action_batch)
                        # print(loss)
                        # loss = criterion(state_action_values, action_batch.unsqueeze(1))
                        # loss = criterion(market_status, train_status)

                        # Optimize the model
                        loss.backward()
                        for param in train_net.parameters():
                            param.grad.data.clamp_(-1, 1)
                        optimizer.step()
                        print("epoch[%d:%d] epsode is next loss[%.10f]" % (epoch, idx, loss.item()))

                    if idx % TARGET_UPDATE == 0:
                        torch.save(train_net.state_dict(),"pt/train_net_{}.pt".format(device_str))
                    
                    spend = time.time() - since
                    print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                            % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
                    if account.is_bankrupt():
                        break
                    if idx == df.index.max():
                        break
                        # train_net.load_state_dict(train_net.state_dict())
                    # save_file_model(degreeQ)                
                except Exception as ex:
                    exc_type, exc_obj, exc_tb = sys.exc_info()
                    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                    print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

        print("end training DQN")
    
    print('Complete Training')

if __name__ == "__main__":
    main()

main_buroto.py 파일입니다.

import random
from collections import deque, namedtuple

Experience = namedtuple(
    'Experience',
    ('state', 'action', 'reward')
)

class ReplayMemory(object):
    def __init__(self, capacity):
        self.memory = deque([],maxlen=capacity)

    def push(self, *args):
        """Save a transition"""
        self.memory.append(Experience(*args))

    def pop(self, batch_size):
        arr = []
        for _ in range(batch_size):
            arr.append(self.memory.popleft())
        return arr
    
    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)

memory.py 부분을 분리했습니다.

import torch.nn as nn
import torch.nn.functional as F

class DQN(nn.Module):
    def __init__(self, device, h, w, outputs, qsize):
        super(DQN, self).__init__()
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, qsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(qsize)
        self.device = device
        self.conv3 = nn.Conv2d(qsize, qsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(qsize)

        # Number of Linear input connections depends on output of conv2d layers
        # and therefore the input image size, so compute it.
        def conv2d_size_out(size, kernel_size = 5, stride = 2):
            return (size - (kernel_size - 1) - 1) // stride  + 1
        convw = conv2d_size_out(conv2d_size_out(conv2d_size_out(w)))
        convh = conv2d_size_out(conv2d_size_out(conv2d_size_out(h)))
        # print("convw[%d]  convh[%d]" % (convw, convh))
        linear_input_size = 54 * qsize
        self.head = nn.Linear(linear_input_size, outputs)

    # Called with either one element to determine next action, or a batch
    # during optimization. Returns tensor([[left0exp,right0exp]...]).
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        return self.head(x.view(x.size(0), -1))

dqn.py를 분리했습니다.

main함수부터 설명 드리면 가장 이상적인 매매 프로세스인 저점에서 매수하고 고점에서 매도하는 방식을 rolling함수를 이용하여 구하였습니다.
그리고 이것을 전체 데이터에 적용을 하여 history에 저장합니다.
매매 길이에 따라서 수천프로이상의 수익이 발생할 것입니다.
이것은 말 그대로 이상적인 매매 프로세스 입니다.
백프로 매치되는 것은 아니지만 이상적인 grid world의 마르코프 실행 프로세서(MDP) 처럼은 우리도 이것을 MDP로 가정합니다.
이제 실행된 결과를 DQN(CNN)에 학습합니다.
optimize함수가 그 역할을 합니다.
loss함수는 nn.SmoothL1Loss를 사용하여 해당 S의 action a와 기대 이상 행동 a'를 구하여 둘의 차이를 구하여 계산합니다.
아쉽지만 windows에서는 NCCL이 적용되지 않아 pytorch함수에서 에러가 발생합니다.
NCCL은 nvidia의 GPU를 서로 연동하여 최상의 성능을 내는 Api lib입니다.
현재 ubuntu와 red hat에서만 지원하고 있습니다.
그래서 지금 저는 WSL2를 이용한 ubuntu LTS(최종안정화버젼)인 20.04.5버젼으로 이전을 하려고 하는데 windows10과 wsl2의 병행 운영은 엄청난 cpu 제원을 요구 하네요.
어쩔 수 없이 서버를 ubuntu로 다시 설치하고 노트북으로 접속하여 이용하는 방법을 실행하려고 하고 있습니다.
불행히도 제 노트북이 현재 아작이 난 상태라 알리에서 검색해서 LCD판넬을 주문한 상태입니다만 가장 빠른 날짜가 12월 15일 도착이라고 하네요.
Nvidia 홈페이지 상으로는 현재로써는 windows에 NCCL이 적용이 안되어서 여러장의 GPU카드가 있어도 한장의 효과만 있다는 거죠.
현재 소스에서 보시면 nn.DataParallel이 DQN을 감싸고 있습니다.
실제 성능탭으로 보면 그래도 nn.DataParallel을 적용하기 전에는 한개의 GPU만 사용하게 작동이 되었으나 지금은 어느 정도 반반씩 부담하여 사용하는게 보입니다.
중요한 점은

recall_history()함수에서 저장된 데이터를 recall_training 함수에서 DQN을 학습하게 됩니다. 학습된 결과는 pt/training_net.pt파일로 저장을 하게 됩니다. 저의 경우는 데이터가 너무 많은 관계로 과적합 상태를 보이나 로스차이가 그렇게 크지는 않습니다. 적당한 데이터의 크기는 5000~10000개 정도의 데이터를 반복 학습하면서 loss를 최소화 한다음 다시 데이터를 샘플링하고 적용하고 하는 반복 작업을 통하여 실제 DQN이 적은 loss로 동작하는지가 중요합니다. 

이 작업이 어느 정도 끝나면 다음은 main_agent.py 파일을 작성하여 실제 상황에서 detection을 발생하여 거래를 하게 하여 어느 정도의 수익률이 나오는지 분석을 해 보도록 하겠습니다.
저도 처음에는 강화학습이 데이터가 많이 적용되면 더욱 딕텍션이 안정될 거라 생각했지만 실제로는 데이터가 많을 수록 ai는 거래를 하지 않으려하네요.
프로그램을 조금 수정해서 pt파일을 여러 버젼으로 저장하는게 어떨까 제안 합니다.
다음 차에서 agent파일을 돌려 보면 알겠지만 DQN은 가장 안정화된 방법을 선택하려 합니다.
즉 매매를 하지 않아 수익도 손실도 없는 상태를 만들려고 합니다.
그렇지만 어떤 경우 훈련이 잘 진행되어 100%이상의 수익을 내기도 합니다.
아무런 if문 없이 수익을 낸다는 게 신기합니다만 retro.gym을 경험해 보신분들은 아실 겁니다.
사실 retro.gym을 훈련하는 과정에도 인간의 생각 판단 따위는 중요하지 않습니다.
훈련은 우연의 연속입니다.
다만 그 우연들 중에서 가장 스코어가 높은 쪽으로만 AI는 움직이려 합니다.
그렇게 하는것은 인간의 계산에서 epsilon값을 어떻게 판단하는냐에 따라 결과가 달라 질 겁니다.
epsilon을 0.5 정도로 하면 절반은 모험값으로 채워질 겁니다.
그리고 결과를 수익률 또는 남은 돈으로 정할 때 그 값이 최고가 되는 모델들만 history에 저장하고 학습을 돌린다면 엔진은 공격적인 매매를 해서 높은 수익률을 올릴 것입니다.
다음 편 실 agent 시뮬레이션 편을 기대 해 주세요.
 

 

반응형
LIST

맨위로버튼이미지

정현 씨는 클래식 기타리스트입니다. 세계적으로 유명하지는 않지만 그래도 국내에서 꽤 인지도가 있습니다. 지방의 음대에 학생을 교수하기도 하고 가끔 독주회와 지방시향가 공연도 하는 등 그나마 국내에서는 톱스타입니다. 남편은 그러니까 전남편은 첼리스트였습니다. 서울의 유명 음대를 같이 다닌 학교의 유명한 CC였습니다. 한 번도 떨어져 본 적이 없고 파리 유학도 같이 갔습니다. 유학을 가기 전 남편 쪽에서 제의가 있어서 그러니까 시어머니의 권유로 먼저 결혼을 했습니다. 파리의 아파트는 한국의 아파트와는 달랐고 그렇게 낭만적이지도 않았습니다. 사실 기타를 위해서는 미국으로 유학을 가고 싶었지만 그의 남편을 위해서 프랑스로 가는 것을 택했습니다. 남편의 공부는 뜻대로 잘 풀리지 않았습니다. 겨우 공부를 마치고 한국으로 돌아왔을 때 정현 씨와는 달리 남편을 부르는 곳은 많지 않았습니다. 그때쯤 정현 씨에게는 귀여워 애기가 태어났습니다. 그때는 서울 모교에서 부교수 제의가 있어서 그는 욕심을 내서 모교에 교수를 선택했습니다. 사실 유학에 대한 학습효과입니다. 유학 때 포기한 정현 씨의 꿈은 결과를 내지는 못했습니다. 모든 것이 그렇듯 어느 이상이 되면 더 이상 오를 수 없는 절벽을 만납니다. 지금 남편의 상태가 꼭 그랬습니다. 남편은 먼 지방의 시향에 자리를 잡았습니다. 남편 집안에서는 말이 많았지만 결과는 어쩔 수가 없었습니다. 그렇게 주말 부부가 되었지만 모든 육아의 짐은 올곧이 정현 씨에게 맞겨졌습니다. 정현씨의 친정도 어느정도의 재력을 가지고 있었고 또 정현씨의 어머니는 손녀를 너무 나도 이뻐하셔서 주 오일을 정현씨의 집에서 계셨고 일을 도와주시는 이모님도 구해 주셔서 육아가 완전히 힘든건 아니지만 밤에 애기에게 수유를 하는 것은 너무도 힘들었습니다. 생활의 리듬이 다 깨져 있었지만 그나마 학교에 있는 동안은 정말 행복했습니다. 학부 시절도 그랬지만 이 맘때의 학교 교단가 창가에 비치는 햇쌀은 정현씨를 너무도 행복하게 만들었습니다. 남편의 외도를 눈치챈것은 이미 오래 전 입니다. 주말 부부로 지내는 동안 남편은 지방에서 다른 여자를 만나고 있었습니다. 주말에 서울로 오면 거의 잠만 자고 애기에게도 크게 관심이 없는 듯 보였습니다. 일요일 오후가 오면 남편은 지방행 기차에 몸을 실습니다. 남편을 기차역까지 배웅하고 차로 돌아오는 길에 외도하는 남편을 상상했지만 그렇게 어떤 감정이 있지는 않았습니다. 다음날 학교에서 연습을 하는데 악보에 오선들이 춤을 춥니다. 음표들도 오선을 떠나 자유롭게 날아 다닙니다. 너무나도 신기하고 재밌는 광경이라고 생각했습니다. 마치 지금 지친 육아로 잠시 낮잠을 자고 있는게 아닌가 생각이 들었지만 그런건 아니었습니다. 현실이었습니다. 음표를 따라 연주를 하다가 화음이 맞지 않는 음이라고 생각이든 음을 피하려다 다른 음을 급히 눌렀습니다. 그리고는 움직이는 악보를 꼼작 못하게 체포라도 하려는 듯이 손가락으로 그 마디를 짚었습니다. 아까는 미쳐 못봤지만 그 음에는 '샾'이 붙어 있었습니다. 하지만 방금전에 음도 썩였을 때 아주 풍부한 느낌을 주는 것 같아 연필로 조그맣게 악보에 그렸습니다. 그리고 또 일주일이 지나 역으로 갈때 정현씨는 이제 껏 참았던 말을 했습니다. "언제부터에요?" 남편은 무슨 소리냐는 듯한 모습으로 그녀를 쳐다 보았습니다. 한참의 침묵이 흐른 뒤 남편은 모든 것을 포기한 사람처럼 정현씨에게 물었습니다. "언제부터 알고 있었어?" 정현씨는 속으로 어처구니가 없다는 생각을 했지만 말하지 않았습니다. 남편은 조용히 차문을 열고 내렸습니다. 정현씨 또한 말없이 트렁크문을 열었습니다. 그리고 차문으로 서류 봉투 하나를 내밀었습니다. 남편이 말없이 서류 봉투를 받자 정현씨는 말없이 차를 돌렸습니다. 근데 기분은 하나도 슬프지 않은데 눈물이 의지와 상관없이 흘렀습니다.울음을 소리내지는 않았지만 눈물은 멈출 수 없었습니다. 그리고 일주일이 흘렀지만 남편은 나타나지 않았습니다. 그리고는 문자가 왔습니다. 남편이 합의 이혼서류를 법원에 접수했다는 문자였습니다. 학교 레슨실에서 연습을 하고 있는데 또 그때의 현상이 나타났습니다. 악보가 춤추는 것은 신기하지만 더 이상 참을 수는 없었습니다. 친구 남편이 하는 좀 큰 안과를 찾아 갔습니다. 차를 가지고 갔는데 의사선생님이 안약을 눈에 넣을 건데 하루 정도는 운전 같은 건 위험하다고 합니다. 그때 정현씨가 왔다는 소식을 듣고 친구가 왔습니다. 뒤에서 모든 이야기를 다 들은 사람처럼 잘 됐다며 오래 만에 집으로 가서 수다나 떨자고 합니다. 그리고는 여러가지 검사가 진행되었고 잠시 후 정현씨는 다시 진료실로 왔습니다. "건성 황반변성입니다" 정현씨는 처음 들어보는 병명에 "네?"라고 반문했습니다. 눈의 수정체가 카메라의 렌즈라면 망막은 카메라의 상이 매치는 곳인데 여기에 어떤 이유로 변형이 온것이라고 의사 선생님이 설명했지만 무슨 말인지 머리 속에서 정리가 되지는 않았습니다. 그렇지만 의사선생님의 표정으로 뭔가 심각하다는 건 알 수 있었습니다. 병은 빠르게 진행이 되어 여름이 지날 쯤에 이미 시력의 50%정도를 잃었습니다. 사물의 형태는 볼 수 있지만 빛이 없는 곳에서는 그것도 힘들었습니다. 시력을 거의 잃었을 때쯤 그래도 학교는 정현씨를 학교에 남게 해주었습니다. 그리고 10년 정도의 시간이 흘렀습니다. 어느 지방의 시향과 공연을 하게 되었습니다. 두 곡 정도를 연습했습니다. 원래도 그랬지만 악보를 볼 생각은 꿈도 꿀 수 없게 되었습니다 시력을 잃고 귀는 더 좋아 진것 같습니다. 연주회 당일입니다. 애기였던 딸은 벌써 초등학교 6학년입니다. 오늘은 딸이 정현씨의 눈과 지팡이가 되어 연주회 자리에 앉혀 주었습니다. 연주는 열린 음악회식으로 지휘자 선생님이 곡을 설명해 주었습니다. 지휘자 선생님이 곡을 설명할때 주변의 술렁임을 눈치챌 수가 있었습니다. 두 곡을 마치고 마이크가 정현씨에게 왔습니다. 그래도 빛을 느낄 수 있어 빛이 밝게 빛나는 곳을 향해 인사를 하고는 말을 하기 시작했습니다. 오케스트라 를 향해 "첼리스트분이 누구시죠?"라고 말을 하고는 관중들에게 박수를 능청스럽게 청했습니다. 그러자 첼리스트가 마지 못해서 자리에서 인사를 했습니다. 그리고 말을 이어갔습니다. "시력을 잃고 귀는 더 예민해졌습니다. 나는 그때 모든 것을 잃었다고 생각했는데 하나님은 나에게서 아무것도 가져가시지 않았습니다. 기타를 연주하는 것도 더 편해진 것 같아요. 시력을 잃고는 악보에 있는 노래들보다 더 풍성하게 연주할 수 있게 되었습니다. 아까 첼리스트 분은 제가 연주를 틀렸다고 생각했나 봅니다. 곡이 좀 어려워지면 첼로의 음이 너무 날까로워져서 웃음을 계속 참아야 했습니다" 첼로 소리가 들렸던 방향을 향해서 말했습니다. "나는 잘하고 있으니 당신이나 잘해주시겠어요" 관중에서 웃음소리가 떠져나왔습니다. 오늘 딸에게는 아주 특별한 날입니다. 태어나 처음으로 아빠를 보는 날이었습니다. 딸이 어디쯤 안아 있는지 정현씨는 금방 알 수 있었습니다. 모두가 웃고 있는데 한 방향에서 소리를 죽인 울음 소리를 들을 수 있었습니다. 정현씨가 딸을 데려온 이유를 딸은 엄마가 말하지 않아도 알 수 있었습니다. 밖에서는 둘을 기다리는 할아버지와 할머니가 있었습니다. 정현씨의 엄마,아빠입니다. 엄마는 그날따라 눈물을 흘립니다. 정현씨는 딸을 꼭 안아주었습니다. 그 들의 차가 사라지는 곳을 이젠 중년이 된 첼리스트가 말없이 보고 있었습니다. 정현씨 무리는 가을로 가고 있었지만 첼리스트는 겨울을 느끼고 있었습니다. 같은 장소지만 두 사람은 가을로 겨울로 가고 있었습니다.

반응형
LIST

'일상의 생각 > 자작 소설' 카테고리의 다른 글

다른듯 같은것(1)  (0) 2022.12.19
삶 그리고 가족  (1) 2022.12.17
코마(끝나지 않는 기억)  (0) 2022.12.13
사랑이란 이름의 오빠  (2) 2022.12.11
식구를 늘리는 냥이  (0) 2022.11.18

맨위로버튼이미지

우리가 우리 생의 모든 부분을 태어날 때부터 죽을 때까지를 다 알고 있다면 우리는 과연 어떤 모습으로 살고 있을 까요?
타임루프 물 애니나 영화에서는 그런 장면이 많이 나옵니다. 자신의 절대 위기나 문제를 이미 경험해 보았기 때문에 모든 문제를 피해 갑니다. 니콜라스 케이지의 2007년 작 넥스트라는 작품이 있습니다. 실제 이 영화는 톰 크루저 주연의 마이너리티 리포트와 같은 소설을 원작으로 하고 있지만 내용은 완전히 다릅니다. 니콜라스 케이지는 한 여자를 운명적으로 좋아하지만 그녀를 만나게 되면 세상은 적어도 미국은 핵폭발로 멸망하게 됩니다. 영화는 니콜라스 케이지의 3분 전을 내다보는 예지 능력의 이야기를 다루지만 2시간 동안의 이야기의 내용은 그래서 아무 일도 안 하고 안 일어났다는 거구 니콜라스 케이지는 몇 번이나 한 여자를 만나서 사랑을 하지만 그녀는 한 번도 니콜라스를 만나지 않았다는 내용의 끝은 좀 허무한 그런 내용입니다. 단 3분 앞을 내다보지만 그 3분만으로 니콜라스 케이지는 아주 쉽게 돈을 벌고 아주 쉽게 FBI를 따 돌리고 세상을 구하지만 결말은 항상 핵폭발이라 그냥 포기하고 혼자 외로운 인생을 산다는 그런 이야기입니다. 우리도 니콜라스 케이지 정도의 예지력은 아니지만 저는 upbit의 이더리움 데이터 1년 6개월치를 가지고 있습니다. Market class의 get_data함수를 보면 아래 부분이 추가되어 있습니다. 주식이던 코인이던 가장 많은 돈을 버는 방법은 가장 고점에서 팔고 가장 저점에서 사고를 계속하면 무조건 돈을 번다는 거죠. python의 rolling함수를 이용하여 앞으로 50 뒤로 50 범위의 수중 가장 높은 값을 미리 구하고 index를 계속 돌리면서 사고팔고를 계속하고 그 중간에는 관망을 하면 어떤 일이 있을 가요? 거의 천문학적인 돈을 벌 수 있습니다. 이런 일이 안 일어나는 이유는 50개의 데이터가 예지의 영역에 있기에 실제로는 가져올 수 없는 값이라는 거죠.

        self.df['closemax'] = self.df.close.rolling(window=100, center=True).max()
        self.df['closemin'] = self.df.close.rolling(window=100, center=True).min()​

위의 내용에서 closemax와 closemin을 미리 계산합니다. 아래 select_action은 제가 말씀 드린 내용으로 매매를 하지만 epsilon값을 구하여 그 값의 하향선에서만 정책적인 매매를 하고 그 이상이 되면 random행동을 한다는 거죠. 예를 들면
섬에 표류한 한 사람이 그 섬에 있는 5가지 열매 중 가장 첫번째 먹어서 죽지 않은 음식을 구출될 때까지 계속 먹게 된다는 이야기와 같은 원리로 머쉰 러닝의 결과도 한 가지 결과 중에서 최고값이 나오면 더 이상은 다른 값을 찾아가지 않습니다. 그래서 프로그래머는 강제로 모험을 하는 확률을 넣어서 모험을 실행하는 계수만큼 모험을 하게 만들어 진짜 최적의 값이 지금 현재 머쉰 러닝이 찾아낸 값인지 계속 질문을 합니다. 아래 함수는 실제 거래에서는 불가능하지만 우리는 이미 거래의 결과를 다 알고 있기 때문에 아래의 방식으로 최고 수익을 내는 방법을 history Series에 저장할 겁니다. 아래는 그 소스입니다. 아래의 행동을 policy action(정책적인 행동)이라고 합니다. 이 부분은 DQN과는 아무 관련이 없으며 인간이 최적의 경로를 찾아가는 MDP(마르코프 실행 프로세스)를 주식이나 코인의 차트에 대입해 본 것입니다. 자 이제는 우리는 최적의 방법으로 미친 수익률을 내는 history를 찾을 수 있습니다. 현실에서는 불가능 하지만 몇천 배의 수익을 발생하게 됩니다. 그야말로 이대로만 되며 누구나 부자가 될 수 있게 죠.

def select_action(df, idx):
    try:
        global steps_done
        sample = random.random()
        eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
        steps_done += 1
        if sample > eps_threshold:
            if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
                action = 2
                return  action
            elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
                action = 1
                return  action
            else:
                return 0
        else:
            action = random.randrange(action_kind)
            return  action
    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
        return 0
  
  def get_chart(market, df, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    if  idx > (df.index.max() - 299):
        return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # display(img)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    chart = converter(img).unsqueeze(0).to(device)
    return chart
  
 def recall_history(df, account, idx, re_action):
    max_price = -np.inf
    min_price = np.inf
    max_reward = -np.inf
    max_rew_step = 0
    min_rew_step = 0
    save_point_idx = 0
    account.reset()
        
    for _, idx in enumerate(df.index, start=idx):
        try:
            max_step = idx
            since = time.time()
            if re_action != 0:
                action = re_action
                re_action = 0
            else:
                action = select_action(df, idx)
            
            print("idx:%d==>policy_action:%d"%(idx, action))

            reward, real_action = account.exec_action(action, idx)
            history.loc[idx, "action"] = real_action
            max_reward = reward if reward > max_reward else max_reward
            history.loc[idx, 'max_rew'] = max_reward

            if df.loc[idx, 'close'] > max_price:
                max_price = df.loc[idx, 'close']  
                account.back_up()
                max_rew_step = 0
                save_point_idx = idx
            else:
                max_rew_step += 1
            
            if df.loc[idx, 'close'] < min_price:
                min_price = df.loc[idx, 'close']
                account.back_up()
                min_rew_step = 0
                save_point_idx = idx
            else:
                min_rew_step += 1
            
            # if account.unit > 0 and max_rew_step > 50:
            #     return max_step, True, save_point_idx, 2
            # elif account.unit == 0 and min_rew_step > 50:
            #     return max_step, True, save_point_idx, 1
            
            if account.is_bankrupt():
                break
            spend = time.time() - since
            print("idx:%d used time[%.2f] price[%.2f] unit[%.2f] agent rate:%.05f remind money:%.02f" 
                    % (idx, spend, df.loc[idx, 'close'], account.unit, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
            # if epoch % TARGET_UPDATE == 0:
            #     target_net.load_state_dict(train_net.state_dict())
            # torch.save(train_net.state_dict(),"pt/train_net.pt")
            # save_file_model(degreeQ)                
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            print("`recall_history -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    return max_step, False, 0, 0

다음 차수에는 드디어 DQN(CNN) 학습으로 Q(s, a) 함수를 구현해 보도록 하겠습니다. 이전 차수의 상태 값 Sn이 바로 env입니다. 이미 우리는 env를 구했습니다. 그다음은 env에 해당 행동을 적용 했을때 기대값과 실행 후의 상태 Sn을 구해야 하는 문제가 남게 되는데 그것은 Account 클래스를 통해서 구현 해 보도록 하겠습니다. 프로세스는 2개가 동시에 실행되어야 합니다. 지금 우리가 만들고 있는 프로그램은 제 기준으로 1주일에 1 epoch를 도는 굉장히 느린 프로그램입니다. 돈이 좀 더 있다면 DQN 계산 능력을 향상 시키겠지만 현재로는 한 스텝에 평균 8초 정도 걸립니다. 이 시간은 차트 Cn - C(n-1)을 해서 상태 Sn을 구하고 이것을 CNN으로 학습하고 loss를 구하여 optimize까지 실행한 결과 입니다.
방식은 위의 이상적인 history를 먼저 실행하여 실행 행동의 행렬을 구하고 그 다음은 DQN학습을 진행하는 방식입니다.
이렇게 학습된 모델은 어떤 특정 디렉터리(pt/training_net.pt) 파일로 저장이 되고 학습을 전혀 하지 않고 DQN으로 행동만을 받아서 실행하는 또 다른 프로세스에서 모델 파일을 읽어서 셋업을 한 다음 실 데이터로 거래를 하는 형식으로 구성이 될 겁니다. 처음에는 진짜 리얼 데이터가 아니라 학습 데이터로 실행을 할 것이고 이후 학습이 되는대로 실 데이터로 실전을 할 것입니다.

 

 

반응형
LIST

맨위로버튼이미지

주식이나 코인 매매 프로그램을 너무 쉽게 생각하시면 큰일 납니다. 제 경험이기도 합니다. 일단 머쉰 러닝을 배우신 분들 중에는 주식차트를 시계열 데이터로 착각하시는 분들이 많습니다. 물론 시계열 데이터이기도 합니다. 시계열 데이터의 가장 큰 특징은 무엇일까요? 정답을 아시는 분들도 있겠지만 바로 계절성입니다. 예를 들면 따릉이의 이용률을 데이터로 변환한다면 봄가을에는 많은 사람들이 이용해서 자전거가 부족하지만 여름이나 겨울에 그 수가 확실히 감소합니다. 특히 겨울에는 이용률이 크게 감소하죠. 따릉이 자전거를 자동 배치하는 AI를 개발한다면 크게 어렵지 않게 개발이 가능하겠죠. 그렇다면 시계열 데이터 방식으로 예를 들면 LSTM 방식으로 삼성전자의 주가를 예측해보신 분들은 나름 데이터가 맞는 것 같아서 신기해합니다. 하지만 그것은 예측된 데이터로 보기엔 문제가 있습니다. 인간이 개입하는 어떤 데이터도 시계열 데이터의 특징인 주기성을 보이지는 않습니다. 그저 서로를 속이는 눈속임일 뿐입니다. 아쉽게도 LSTM이나 RNN, GRU 방식 등의 방식으로 주가를 예측하는 것은 애초에 불가능 합니다.https://m.blog.naver.com/dsz08082/222042702104
위 url의 내용을 한번 참고해 보세요. 앞에서도 말씀드렸듯이 주식, 코인을 자동으로 매매하는 프로그램은 정말 쉽지 않습니다. 인터넷에 통장까지 공개하시는 분들이 많은데 이 번의 사태를 견뎠을까요? 주식 또한 마찬가지로 동일본 지진이나 리먼사태 등을 예측할 수는 없었습니다. 그렇다면 강화 학습으로 그런 문제를 극복할 수 있을까요? 사실 저도 자신이 없네요. 그래서 브레이크 장치가 필요합니다. 그래도 주식은 제도권 안에 있기에 법적인 구제 장치가 많지만 코인 시장은 그렇지 않습니다. 혹시나 인터넷이나 유튜브를 떠도는 프로그램들에는 눈도 돌리지 마시기 바랍니다. 제가 쓴 글들 중에도 몇몇 것은 참담한 실패를 맛봤습니다. 가끔 사람은 세상을 너무 만만하게 보거나 짧은 생각으로 글을 쓸 수 있습니다. 지금 우리가 하는 길이 만만하지 않음을 꼭 아셨으면 합니다. 오늘은 CNN과 상태(S)와의 관계에 대하여 생각해보겠습니다. 코인 차트는 시간(t)에 따라 C0
, C1, C2, ... , Ct-1, Ct, ... 이렇게 생성이 될 겁니다. pytorch 의 tensor데이터는 다차원의 행렬로 이루어져 있습니다. PIL이미지를 torch.tensor로 변환하는 함수를 지원합니다. 차트 이미지를 torch.tensor로 변경했다면 C2 - C1을 하면 t1의 상태(S)가 됩니다. 원래는 RGB데이터로 이미지를 저장하고자 했지만 저도 이미지 쪽은 잘 몰라 ARGB방식 즉 4차원 torch.tensor로 저장이 되었습니다. CNN은 합성곱 인공신경망의 약자입니다. 이미지의 특징을 검색하기 위해 bmp방식으로 저장하게 되면 메모리 overflow로 프로그램은 더 이상 진행하지 않고 런타임 에러가 나거나 엄청난 컴퓨터 비용을 지불해야 할 것입니다. 그래서 화면의 일부를 차원곱을 사용하여 이미지를 쭉 따라서 검색하는 방식으로 이미지를 분석합니다. 인터넷에 있는 개와 고양이를 구분하는 예제는 고양이 코 모양과 개의 코 모양이 완전히 다르다는 것을 신경망 학습이 비지도 학습으로 알아내고 클래스(분류 가짓수)를 2라고 한정했기 때문에 고양이의 코 모양을 0으로 했다면 개의 코 모양은 1로 해서 분류를 하고 학습을 반복할수록 지도 학습으로 정확도를 높이게 됩니다. 학습이 끝나면 거의 100%의 정확도로 고양이와 개를 구분합니다. 즉 이미지의 모든 부분을 이용하는 것이 아니라 특징이 다른 부분을 합성곱으로 빨리 탐색하고 분류하는 것이죠. 다시 주식차트로 오면 C2 - C1을 하게 되면 C2와 C1의 동일한 부분은 모두 0이 될 겁니다. 행렬의 차는 같은 위치의 값을 서로 빼는 것이니 당연히 같은 좌표에 있는 이미지 중에 같은 값을 가진 것들은 전부 0으로 바뀔 겁니다. 그럼 C2 - C1을 S1이라고 하면 대분은 0인 이미지가 나오겠죠. 그리고 변화가 생긴 부분만 남게 됩니다. 그래서 우리가 만드는 차트에 라벨링은 의미가 없습니다. 같은 위치에 반복적으로 쓰이는 값은 삭제될 뿐입니다. 그다음 S1을 합성곱으로 검색해서 지나가는 부분은 비지도 학습에 의해 가짓수를 CNN이 결정합니다. 다시 우리가 지정한 output개수 즉 class의 가짓수에 의해서 다시 CNN은 지도 학습을 진행하면서 각각의 클래스에 해당하는 숫자를 지정하게 됩니다. 학습을 진행하면 우리 인간은 눈치 못 챈 어떤 특징으로 인해 차트는 우리가 지정한 가지 수만큼의 output값을 지정하게 됩니다. 우리는 이 값을 행동 0,1,2로 지정합니다. 저희 프로그램은 100% 매수 또는 매도 또는 관망의 3가지 행동 뿐입니다. CNN은 우리의 의도대로 차트를 가지고 우리가 실행해야 하는 행동을 구하게 됩니다.
어렵지 않게 설명하려고 많이 노력했는데 괜찮았는지 모르겠네요. 상태 값과 CNN의 관계를 이해하셨다면 마르코프 실행 프로세스에 해당하는 부분을 설명하겠습니다. 혹시 오류를 발견하셨다면 댓글 부탁드립니다.

 

 

반응형
LIST

맨위로버튼이미지

지금은 코인을 중심으로 프로그램을 작성하지만 주식에도 그대로 적용이 가능합니다. 코인 거래소는 upbit를 이용하고 있습니다. 지금은 코인원이 수수료가 가장 싸므로 api를 조금 수정하시면 코인원식으로 변경이 가능 할 겁니다. 강화학습에서 중요한 포인트는 env와 agent입니다. 코인에서는 env는 마켓으로 agent는 account라고 저는 명명을 했습니다. 꼭 그렇게 않해도 됩니다. 여기에 게시되는 소스는 아무런 라이센스 없이 사용하셔도 됩니다. 우리가 사용할 DQN에 대하여 간단하게 설명 드리겠습니다. 그전에 참고할 만한 책을 한권 소개 드립니다. '바닥부터 배우는 강화학습'(https://www.googleadservices.com/pagead/aclk?sa=L&ai=DChcSEwj6gIbC29D7AhVy3EwCHXJfAU4YABAKGgJ0bQ&ohost=www.google.com&cid=CAESbeD2J1z4qWevI6iWwrPeahjlRAd5PHQxq3vxCnxSB-XFcQcQlPCdbI5vS31FJsWDEdYDXr7wQRya94UUUP1TeQ_sv2Gp1efEdpeYMlECKaQ1jwRuNHUqGOxGxPQPMtaqjppqY8HY39-QdAnHBII&sig=AOD64_1r3tIroFWvKdgd3IEQB1BX0mmAdg&ctype=5&q=&ved=2ahUKEwizuv_B29D7AhUVBd4KHTcyApkQ9aACKAB6BAgEEAw&adurl=)

을 한번 읽어 보시기 바랍니다. 아주 기초 이론이고 실제 강화학습은 이 책 내용과는 다를 수 있습니다. 강화학습의 그리드월드 내용을 보시면 약간 지도학습의 모습과 닮았습니다. 모든 강화학습의 방법은 책 내용데로 해서는 결과를 보기는 상당히 힘듧니다. 기본적으로 agent는 일단 인공신경망을 배제하고 env와 agent를 구성할 필요가 있습니다. 그리고 나서 실제 agent가 발전하는게 확인이 되면 그다음은 인공신경망을 이용하여 Q(s,a)함수를 Deep learning 모듈화 하는 것입니다. 사실 이 모든 것은 어떤 수학자가 주장한 모든 문제는 곡선화 할 수 있으며 곡선화를 하면 해당 답을 구할 수 있다는 일반 함수론에서 출발하여 Q-learning을 DQN을 이용하여 함수로 만들 수 있다는 개념으로 출발했습니다. 그러니 Q(s,a)함수를 먼저 구하고 그 다음 그 함수를 DQN모듈로 변환하는 것이 바로 강화학습인것이죠. 물론 DB를 사용할 수도 있을 것입니다. 아주 정밀한 값도 저장할 수 있지만 만약에 데이터가 무한하다면 DB로 그 많은 데이터를 저장할 수 있을 까요? 그래서 강화학습에서는 DQN(CNN)을 이용하여 함수화 하자는 거죠 토치의 저장기능을 사용하면 몇 byte의 값으로도 그 함수를 저장할 수 있으니까요? 다시 시작할 때는 그냥 그 모듈을 다시 읽어서 재실행하면 끝입니다. DB보다는 컴퓨터 비용이 훨씬 저렴하죠 아니 저렴한 정도가 아니라 거의 공짜 수준이라고 할 수 있죠?
일단 Market class 소스를 보시죠

반응형
from matplotlib.backend_bases import FigureCanvasBase
import py
import plotly.graph_objects as go
import plotly.subplots as ms
import plotly.express as px
import plotly as plt
import pymysql
import pandas as pd
import numpy as np
import time
import talib
from PIL import Image

ticker = 'ETH'
class Market():
    def __init__(self) -> None:
        self.df = pd.DataFrame()
    
    def add_bbands(self):
        try:
            print("볼리져 밴드 구하기:%s"  % time.ctime(time.time()))
            self.df['ma20'] = talib.SMA(np.asarray(self.df['close']), 20)
            self.df['stddev'] = self.df['close'].rolling(window=20).std() # 20일 이동표준편차
            upper, middle, lower = talib.BBANDS(np.asarray(self.df['close']), timeperiod=40, nbdevup=2.3, nbdevdn=2.3, matype=0)
            self.df['lower']=lower
            self.df['middle']=middle
            self.df['upper']=upper
            # self.df['bbands_sub']=self.df.upper - self.df.lower
            # self.df['bbands_submax'] =  self.df.bbands_sub.rolling(window=840, min_periods=10).max()
            # self.df['bbands_submin'] =  self.df.bbands_sub.rolling(window=840, min_periods=10).min()
            # self.df['bbs_deg'] = (self.df.mavg - self.df.mavg.shift()).apply(lambda x: math.degrees(math.atan2(x, 40))) 

            # self.df['pct8']=(self.df.close - self.df.lower)/(self.df.upper - self.df.lower)
            # self.df['pct8vsclose']=self.df.close - self.df.pct8
        except Exception as ex:
            print("`add_bbands -> exception! %s `" % str(ex))

    def add_relative_strength(self):
        try:
            print("상대 강도 지수 구하기:%s"  % time.ctime(time.time()))
            rsi14 = talib.RSI(np.asarray(self.df['close']), 14)
            self.df['rsi14'] = rsi14
        except Exception as ex:
            print("`add_relative_strength -> exception! %s `" % str(ex))
            
    def add_iip(self):
        try:
            #역추세전략을 위한 IIP계산
            self.df['II'] = (2*self.df['close']-self.df['high']-self.df['low'])/(self.df['high']-self.df['low'])*self.df['volume']
            self.df['IIP21'] = self.df['II'].rolling(window=21).sum()/self.df['volume'].rolling(window=21).sum()*100            
        except Exception as ex:
            print("`add_iip -> exception! %s `" % str(ex))
            
    def add_stock_cast(self):
        try:
            # 스토캐스틱 구하기
            self.df['ndays_high'] = self.df['high'].rolling(window=14, min_periods=1).max()    # 14일 중 최고가
            self.df['ndays_low'] = self.df['low'].rolling(window=14, min_periods=1).min()      # 14일 중 최저가
            self.df['fast_k'] = (self.df['close'] - self.df['ndays_low']) / (self.df['ndays_high'] - self.df['ndays_low']) * 100  # Fast %K 구하기
            self.df['slow_d'] = self.df['fast_k'].rolling(window=3).mean()    # Slow %D 구하기        except Exception as ex:
        except Exception as ex:
            print("`add_stock_cast -> exception! %s `" % str(ex))
    
    def add_mfi(self):
        try:
            # MFI 구하기
            self.df['PB'] = (self.df['close'] - self.df['lower']) / (self.df['upper'] - self.df['lower'])
            self.df['TP'] = (self.df['high'] + self.df['low'] + self.df['close']) / 3
            self.df['PMF'] = 0
            self.df['NMF'] = 0
            for i in range(len(self.df.close)-1):
                if self.df.TP.values[i] < self.df.TP.values[i+1]:
                    self.df.PMF.values[i+1] = self.df.TP.values[i+1] * self.df.volume.values[i+1]
                    self.df.NMF.values[i+1] = 0
                else:
                    self.df.NMF.values[i+1] = self.df.TP.values[i+1] * self.df.volume.values[i+1]
                    self.df.PMF.values[i+1] = 0
            self.df['MFR'] = (self.df.PMF.rolling(window=10).sum() / self.df.NMF.rolling(window=10).sum())
            self.df['MFI10'] = 100 - 100 / (1 + self.df['MFR'])    
        except Exception as ex:
            print("`add_mfi -> exception! %s `" % str(ex))

    def add_macd(self, sort, long, sig):
        try:
            print("MACD 구하기:%s, sort:%d, long:%d, sig:%d"  % (time.ctime(time.time()), sort, long, sig))
            macd, macdsignal, macdhist = talib.MACD(np.asarray(self.df['close']), sort, long, sig) 
            self.df['macd'] = macd
            # self.df['macdmax'] = self.df.macd.rolling(window=840, min_periods=100).max()
            # self.df['macdmin'] = self.df.macd.rolling(window=840, min_periods=100).min()
            # self.df['macd_max_rate'] = self.df.apply(lambda x: (x['macdmax'] - x['macd']) * 100 / (x['macdmax'] - x['macdmin']), axis=1)
            # self.df['macd_min_rate'] = self.df.apply(lambda x: (x['macd'] - x['macdmin']) * 100 / (x['macdmax'] - x['macdmin']), axis=1)
            # self.df['prv_macd_degrees'] = self.df.macd_degrees.shift()
            self.df['signal'] = macdsignal
            self.df['flag'] = macdhist
            # self.df['prv_osc_degrees'] = self.df.osc_degrees.shift()
        except Exception as ex:
            print("`add_macd -> exception! %s `" % str(ex))

    def get_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            'time'
            ,`start` as open
            , high
            , low
            , close
            , volume
            , macd
            , macdmax
            , macdmin
            , `signal`
            , `flag`
            , osc_degrees
            , sma1200
            , sma1200_degrees
            , wma1200
            , wma1200_degrees
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.df['sma3'] = talib.SMA(np.asarray(self.df['close']), 3)
        self.df['closemax'] = self.df.close.rolling(window=100, center=True).max()
        self.df['closemin'] = self.df.close.rolling(window=100, center=True).min()
        self.add_macd(12,26,9)
        self.add_bbands()
        self.add_relative_strength()
        self.add_stock_cast()
        self.add_iip()
        self.add_mfi()
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_test_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            `start` as open
            , high
            , low
            , close
            , volume
            , macd
            , macdmax
            , macdmin
            , `signal`
            , `flag`
            , osc_degrees
            , sma1200
            , sma1200_degrees
            , wma1200
            , wma1200_degrees
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.add_bbands()
        self.add_relative_strength()
        self.df['clsmin'] = self.df.close.rolling(window=600, min_periods=600, center=False).min()
        self.df['clsmax'] = self.df.close.rolling(window=600, min_periods=600, center=False).max()
        self.df['action'] = self.df.apply(lambda x: 1 if x['close'] > x['up'] else( 1 if x['close'] < x['dn'] else 0), axis=1)
        # self.df.drop(self.df[self.df['action'] == 0].index, inplace=True)
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_lstm_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            `date`
            ,`start` as open
            , high
            , low
            , close
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.df['fclose'] = self.df.close.shift(-1)
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_chart(self, idx, max_data:int=300):
        try:
            df = self.df.head(idx + max_data).tail(max_data)
            df.reset_index(drop=True, inplace=True)
            if df.index.max() < 299:
                return None
            
            candle = go.Candlestick(x=df.index,open=df['open'],high=df['high'],low=df['low'],close=df['close'], increasing_line_color = 'red',decreasing_line_color = 'blue', showlegend=False)
            upper = go.Scatter(x=df.index, y=df['upper'], line=dict(color='red', width=2), name='upper', showlegend=False)
            ma20 = go.Scatter(x=df.index, y=df['ma20'], line=dict(color='black', width=2), name='ma20', showlegend=False)
            lower = go.Scatter(x=df.index, y=df['lower'], line=dict(color='blue', width=2), name='lower', showlegend=False)

            volume = go.Bar(x=df.index, y=df['volume'], marker_color='red', name='volume', showlegend=False)

            MACD = go.Scatter(x=df.index, y=df['macd'], line=dict(color='blue', width=2), name='MACD', legendgroup='group2', legendgrouptitle_text='MACD')
            MACD_Signal = go.Scatter(x=df.index, y=df['signal'], line=dict(dash='dashdot', color='green', width=2), name='MACD_Signal')
            MACD_Oscil = go.Bar(x=df.index, y=df['flag'], marker_color='purple', name='MACD_Oscil')

            fast_k = go.Scatter(x=df.index, y=df['fast_k'], line=dict(color='skyblue', width=2), name='fast_k', legendgroup='group3', legendgrouptitle_text='%K %D')
            slow_d = go.Scatter(x=df.index, y=df['slow_d'], line=dict(dash='dashdot', color='black', width=2), name='slow_d')

            PB = go.Scatter(x=df.index, y=df['PB']*100, line=dict(color='blue', width=2), name='PB', legendgroup='group4', legendgrouptitle_text='PB, MFI')
            MFI10 = go.Scatter(x=df.index, y=df['MFI10'], line=dict(dash='dashdot', color='green', width=2), name='MFI10')

            RSI = go.Scatter(x=df.index, y=df['rsi14'], line=dict(color='red', width=2), name='RSI', legendgroup='group5', legendgrouptitle_text='RSI')
            
            # 스타일
            fig = ms.make_subplots(rows=5, cols=2, specs=[[{'rowspan':4},{}],[None,{}],[None,{}],[None,{}],[{},{}]], shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.01)

            fig.add_trace(candle,row=1,col=1)
            fig.add_trace(upper,row=1,col=1)
            fig.add_trace(ma20,row=1,col=1)
            fig.add_trace(lower,row=1,col=1)

            fig.add_trace(volume,row=5,col=1)

            fig.add_trace(candle,row=1,col=2)
            fig.add_trace(upper,row=1,col=2)
            fig.add_trace(ma20,row=1,col=2)
            fig.add_trace(lower,row=1,col=2)

            fig.add_trace(MACD,row=2,col=2)
            fig.add_trace(MACD_Signal,row=2,col=2)
            fig.add_trace(MACD_Oscil,row=2,col=2)

            fig.add_trace(fast_k,row=3,col=2)
            fig.add_trace(slow_d,row=3,col=2)

            fig.add_trace(PB,row=4,col=2)
            fig.add_trace(MFI10,row=4,col=2)

            fig.add_trace(RSI,row=5,col=2)

            # 추세추종
            # trend_fol = 0
            # trend_refol = 0
            # for i in df.index:
            #     if df['PB'][i] > 0.8 and df['MFI10'][i] > 80:
            #         trend_fol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='orange', marker_size=20, marker_symbol='triangle-up', opacity=0.7, showlegend=False)
            #         fig.add_trace(trend_fol,row=1,col=1)
            #     elif df['PB'][i] < 0.2 and df['MFI10'][i] < 20:
            #         trend_fol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='darkblue', marker_size=20, marker_symbol='triangle-down', opacity=0.7, showlegend=False)
            #         fig.add_trace(trend_fol,row=1,col=1)

            # 역추세추종
            # for i in df.index:
            #     if df['PB'][i] < 0.05 and df['IIP21'][i] > 0:
            #         trend_refol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='purple', marker_size=20, marker_symbol='triangle-up', opacity=0.7, showlegend=False)  #보라
            #         fig.add_trace(trend_refol,row=1,col=1)
            #     elif df['PB'][i] > 0.95 and df['IIP21'][i] < 0:
            #         trend_refol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='skyblue', marker_size=20, marker_symbol='triangle-down', opacity=0.7, showlegend=False)  #하늘
            #         fig.add_trace(trend_refol,row=1,col=1)    

            # fig.add_trace(trend_fol,row=1,col=1)
            # 추세추총전략을 통해 캔들차트에 표시합니다.

            # fig.add_trace(trend_refol,row=1,col=1)
            # 역추세 전략을 통해 캔들차트에 표시합니다.
            
            # fig.update_layout(autosize=True, xaxis1_rangeslider_visible=False, xaxis2_rangeslider_visible=False, margin=dict(l=50,r=50,t=50,b=50), template='seaborn', title=f'({ticker})의 날짜: ETH [추세추종전략:오↑파↓] [역추세전략:보↑하↓]')
            # fig.update_xaxes(tickformat='%y년%m월%d일', zeroline=True, zerolinewidth=1, zerolinecolor='black', showgrid=True, gridwidth=2, gridcolor='lightgray', showline=True,linewidth=2, linecolor='black', mirror=True)
            # fig.update_yaxes(tickformat=',d', zeroline=True, zerolinewidth=1, zerolinecolor='black', showgrid=True, gridwidth=2, gridcolor='lightgray',showline=True,linewidth=2, linecolor='black', mirror=True)
            # fig.update_traces(xhoverformat='%y년%m월%d일')
            # size = len(img)
            # img = plt.io.to_image(fig, format='png')
            # canvas = FigureCanvasBase(fig)
            # img = Image.frombytes(mode='RGB', size=(700, 500), data=fig.to_image().to, decoder_name='raw')
            # img = Image.frombytes('RGBA', (700, 500), plt.io.to_image(fig, format='png'), 'raw')
            # img = Image.fromarray('RGBA', (700, 500), np.array(plt.io.to_image(fig, format='png')), 'raw')
            # img = Image.fromarray(np.array(plt.io.to_image(fig, format='png')), 'RGB')
            import io
            # img = Image.fromarray(np.array(plt.io.to_image(fig, format='png')), 'L')
            img = Image.open(io.BytesIO(plt.io.to_image(fig, format='png', width=140, height=100)))
            img.convert("RGB")
            img.thumbnail((100, 140), Image.ANTIALIAS)
            # img = Image.Image(fig.to_image(), 'RGB')
            # img.show()
            return img
        except Exception as ex:
            print("`get_chart -> exception! %s `" % str(ex))
            return None

차트 그리기는 https://sjblog1.tistory.com/m/45 의 내용을 참고했습니다. rolling함수를 사용할 경우 속도 문제가 있어서 ta-lib로 변경했습니다. ta-lib의 경우 거의 모든 주식차트 함수를 포함하고 있으며 c++로 개발이 되어 있어서 속도면에서 훨씬 빠릅니다.
클래스를 선언하고 get_chart함수를 호출하면 chart가 bmp 타입(png)의 이미지 데이터로 리턴이 됩니다. 데이터는 저의 경우 DB에 3분 단위로 매일 데이터를 저장하고 있고 그 기간이 벌써 1년반정도 되었씁니다. 꼭 DB를 사용하지 않아도 pyupbit로 데이터를 가져 오면 아마 1주일치 정도의 데이터를 가져 올 수 있습니다. 물론 더 많은 데이터가 있으면 좋겠지만 일주일 정도로도 훈련이 가능합니다. 다음 차에서는 상태값 S를 구하는 것에 대하여 설명 드리겠습니다.

 

 

반응형
LIST

맨위로버튼이미지

내가 본 영화중 탑10에는 들 수 있는 올해최고의 영화인것 같다. 유해진 배우의 전혀 다른 배역 단 아쉬운 점이 유해진 배우의 전작들 대부분이 개그캐 였던것이 독이 된듯 하다 완전 진중한 역에 작은 파도하나가 서사를 무너뜨린다. 영화의 초반부는 스릴러 영화의 재미를 최고로 끌고 있다. 관객들은 숨도 제대로 쉬지 못하고 천봉사의 뒤를 따르고 있었습니다. 반전의 포인트를 너무 빨리 잡은 것이 아닐까 하는 생각이 머리속을 떠나지 않네요. 숨 조차 못 쉴 정도의 서사는 반전 이후 힘없이 무너지고 영화의 방향성을 잃은 건지 극은 종잡을 수 없는 상태가 되어 버린다. 제가 항상 좋아 하던 안은진 배우는 역에 섞이지 못하고 갈 곳을 잃어버리네요. 같이 간 일행도 그 부분을 굉장히 아쉬워하네요. 감독은 연극과 영화의 장점을 살리지 못하고 그 장면을 네보내고 마네요. 후 작업에서라도 수정이 필요해 보이지만 영화는 그대로 진행해버립니다. 이 후 천봉사의 선택과 주변 인물의 역할이 다 정해져 버리면서 더 이상 스릴러로서 재미를 잃어 버립니다. 스페인 영화 인비저블 게스트의 그런 느낌이 참 아쉬운 영화이다. 제 기준 앞부분은 별4개 뒤부분은 별1개 전체적으로는 별 1.5의 점수를 주고 싶다. 엄청 좋은 재료를 보고 훌륭한 요리를 기대했지만 요리에 실망하고 마는 느낌이다. 감독은 송강호 배우의 '변호인'가 스페인 영화 '인비저블 게스트'를 생각했지만 내용은 연극이 되어 버린다.

 

반응형
LIST

'영화이야기' 카테고리의 다른 글

트랜센던스(노 스포)  (1) 2022.12.22
헌트 리뷰(누가 범인) 노 스포  (0) 2022.12.17
웬스데이 리뷰(노 스포)  (1) 2022.12.09
커넥트(신인류의 시작) 리뷰  (0) 2022.12.08
완다비젼 리뷰(노 스포)  (0) 2022.01.12

맨위로버튼이미지

들어가기전에
매몰비용이라는 말이 있습니다. 경제학에서는 '콩코드 효과'라는 말이 있습니다. 미국의 보잉사는 1970년대에도 항공기 제조사중 최고였다고 합니다. 이에 영국과 프랑스는 오로지 미국 보잉을 이기기 위한 싸움을 시작합니다. 그리고 태어난 비행기가 콩코드기 였습니다. 마하2의 속도를 최초로 돌파한 민간 항공기 였을 겁니다. 하지만 앞에서 보는 것보다 뒤로 깨지는 돈이 상당한 그런 사업이었나 봅니다. 사업이 계속 될 수록 투자자들에 얼굴에는 검은 먹구름이 끼고 있었죠. 그리고 콩코드 비행기에 사고가 발생합니다. 콩코드는 보잉의 탑승료보다 훨씬 비싼 비행기여서 일반은 잘 안탑니다. 사고로 유명인사들이 많이 죽게 됩니다. 이로 인해 콩코드 프로젝트는 끝이 납니다. 그 사고가 없었다면 더많은 사람이 죽었을지도 모릅니다.

일단 만약 이 글을 읽는 분이 이 프로젝트를 시작하시고자 한다면 위의 콩코드 효과를 걱정하셔야 할겁니다. 그러나 시작했다면 뒤는 돌아 보시면 안됩니다.
오늘은 개발 환경을 셋팅하도록 하겠습니다. 일단 그래픽카드가 하나 있어야 합니다. 더이상 CUDA는 RTX 10xx대 보더 미만을 지원하지 않습니다. 최소 RTX 1030보더 이상은 구매를 해야한다는 말입니다. 다행히 저가 컴구매자를 위한 저가 모델이 아직 생산되고 있습니다. 최소한 보드의 메모리는 6GB 이상이 필요합니다. 그렇지 않으면 DQN(CNN)모듈이 구동하는 순간 RUNTIME 오류가 발생하고 멈추게 될겁니다. 일단 저희 Lib는 차트를 그릴겁니다. 메모리상으로만 그리고 실제 화면으로 출력 되지는 않을 겁니다. 이 화면의 사이즈는 500 x 700 입니다만 이 사이즈의 메모리도 Pytorch는 처리가 불가합니다. 정확하게는 cuda lib입니다만 바로 memory allocation runtime error가 발생합니다. 내용을 자세히 읽어보시면 gpu 카드의 실메모리량이 부족하여 데이타를 처리할 수 없다는 내용입니다. 그 이유로 차트를 다시 100 x 140으로 축소를 합니다. 나중에는 사이즈를 키울거지만 지금은 이게 최선인것 같습니다. 먼저 ndividia에 회원 가입이 필요합니다. 구글아이디로 가입하는 것이 가장 좋을것 같습니다. ndividia 홈페이지에서 download center로 이동하여 cuda 최신 버전을 다운 받습니다. 13이나 14던 상관은 없습니다. cuda의 설치가 완료되었다면 python 버젼을 선택해야 합니다. 혹시 openAi사의 retro gym을 경험하고자 한다면 python 3.7버젼을 추천합니다. 일단 retro gym을 경험해 보시면 프로그램을 어떻게 해야하는지 감이 옵니다. 어떤 책에서는 강화학습을 env(환경)과 agent(게임을 실행하는 프로그램)으로 나누는 데 retro gym을 보면 python프로그램 자체가 agent가 되고 env는 openAi에서 제공해 준다고 보시면 됩니다. agent를 만들어 게임을 깨는 내 프로그램을 보면 대단하다고 혼자 기쁘하실지 모르지만 진짜는 현실의 상황을 env로 만드는게 진짜구나 하는 생각을 하게 됩니다. 즉 retro gym을 경험하시면 진짜는 DQN이 아니라 env를 어떻게 설계하고 만들지 입니다. 유튜브에 보시면 카이스트 출신의 혁펜다임 채널을 보시면 이론은 쉽게 설명하는 영상을 볼 수 있습니다. 보고 나시면 현실을 강화학습으로 바꾸는 과정의 진짜 중요한 부분은 env라는것을 금방 눈치 챌 수 있습니다. 그러나 다행히 의외로 주식 프로그램은 env 제작이 그렇게 어렵지 않습니다. 앞으로 돌아가서 python 3.9 나 3.10을 사용하면 안되는 이유는 retro gym은 3.9이상에는 실행조차도 안됩니다. 책은 Do it시리즈 중 '강화학습 입문'을 추천 드립니다.( https://ebook-product.kyobobook.co.kr/dig/epd/ebook/4801163032527) 3장의 Buruto force 방법을 보시면 됩니다. 일단은 agent의 개념을 이해하는게 중요합니다. cuda가 다 설치 되었다면 pip와 numpy를 업그레이드합니다

python -m pip install -U pip
python -m pip install -U numpy

torch, torchvision만 있어도 충분하지만 필요하다면 torchaudio를 설치합니다. (https://pytorch.org/get-started/previous-versions/) 이때 torch repository site에 가시면 더 많은 whl(pip install file)이 존재하는것을 보실 수 있습니다. pip에 권한 문제가 생기거나 위치 문제 또는 lib 위치 문제가 발생한다면 python -m pip로 설치 해보시기 바랍니다. 그럼 보통은 대부분 해결이 됩니다. 그리고 pip를 바로 실행할 경우 경로가 꼬여 다시 설치해야 하는 경우가 생깁니다. 그런 경우는 python -m pip install - U {모듈명}를 사용하시면 됩니다. 그리고 whl파일을 설치하는 경우 repository url을 알고 있다면 pip install {url}이렇게 해도 됩니다. torch의 경우 내가 설치한 cuda에 맞는 버젼을 찾아서 설치해야해서 꼭 repository url을 참조해서 설치 하시기 바랍니다. 다음은 ta-lib를 설치해야 합니다 windows 의 경우 구글을 검색하여 whl파일을 python 버젼과 일치하는 파일을 찾아서 설치하셔야 합니다. linux 버젼을 깔 경우는 레드햇 계열의 경우 yum(ubuntu는 apt 또는 apt-get)으로 python버젼과 동일한 python-devel 을 설치해야 하고 gmake와 gcc, g++을 설치 해주셔야 합니다. 설치할 python의 whl파일을 검색하여 버젼과 맞는 repository url을 복사하여 pip문 끝에 붙여 줍니다. 기타 pandas나 다른 util은 쉽게 설치가 되므로 python을 실행해보고 에러유형을 보고 추가하면 됩니다. 다음 장에서는 데이터 수집과 마켓 클래스에 대하여 소개해 드리겠습니다. env의 첫 관문입니다. 강화학습이 궁금하신 분은 retro gym의 예제 하나를 실행해 보고 오시죠. OpenAi는 일론 머스크 돈으로 우리같은 사람을 위해서 만든 오픈소스 플랫폼이니 많이 이용하세요.

 

==> WSL 환경으로 변경하였으니 8장 WSL환경 이용바랍니다.

반응형
LIST

+ Recent posts